> built 2026-10-13 07:12 UTC from 25586fc (master) · wads 0.2.38. Details: build_info.json

# index.html.md

<!-- generated by epythet -->

# wads

wads creates, configures and publishes Python packages: a new project gets a Hatchling `pyproject.toml` and a five-line GitHub Actions stub that calls one shared reusable workflow, and every CI setting lives in `[tool.wads.ci]`. It also migrates legacy `setup.cfg` projects, wires CI secrets, and diagnoses CI failures.

*Still typing code with your own fingers? Skip to [For carbon-based contributors]().*

[![PyPI version](https://img.shields.io/pypi/v/wads.svg)](https://pypi.org/project/wads/)
[![Python versions](https://img.shields.io/pypi/pyversions/wads.svg)](https://pypi.org/project/wads/)

## For AI agents

**Skills.** Twelve agent skills ship inside the package, in `wads/data/skills/`: `setup-py-project`, `wads-migrate`, `wads-repo-doctor`, `wads-ci-health`, `wads-changelog`, `wads-docs-coverage`, `wads-docstring-render`, `wads-import-time`, `wads-pypi-polish`, `wads-skillify`, `wads-test-coverage` and `wads-type-coverage`. Enable them either way:

```bash
pip install "wads[create]" && wads-install-skills   # symlinks them into ~/.claude/skills/
gh skill install i2mint/wads wads-repo-doctor       # one skill, from GitHub
```

`wads-install-skills --list` prints the available names. In a clone of this repo, `.claude/skills/` links every skill for Claude Code, plus the maintainer skill `wads-dev-workflow` (in `skills/`), which covers changing wads itself.

**Project instructions.** [`.claude/CLAUDE.md`]() explains the architecture (pyproject as the single source of truth, the reusable workflow, the two-layer secrets model), the config sections, and the conventions.

**What an agent can do with wads:**

- Scaffold a package with `populate`, or a whole repo with the `setup-py-project` skill.
- Move a legacy repo to `pyproject.toml` and the CI stub with `wads-migrate`.
- Declare CI secrets and env vars with `wads-secrets add NAME`.
- Read or render a repo’s CI configuration from Python, as below.
- Run the CI plan locally with `wads ci-local`, and audit dependency licences with `wads-licence-check`.
- Diagnose a failed run with `wads-ci-debug owner/repo`.

A minimal example, runnable with only the light core (`pip install wads`):

```python
from wads.ci_config import CIConfig
from wads.migration import migrate_ci_to_stub

config = CIConfig(
    {
        "project": {"name": "mypkg", "optional-dependencies": {"dev": ["httpx"]}},
        "tool": {"wads": {"ci": {"testing": {"python_versions": ["3.12"]}}}},
    }
)
assert config.python_versions == ["3.12"]
assert config.project_name == "mypkg"
# A dev extra CI would never install (no [tool.wads.ci.install].extras):
assert config.uninstalled_test_extras == {"dev": ["httpx"]}

stub = migrate_ci_to_stub()  # the ci.yml a new repo gets
assert "uses: i2mint/wads/.github/workflows/uv-ci.yml@master" in stub
assert "PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }}" in stub
```

## What is Wads?

Wads helps you:

- **Create new Python projects** with modern tooling (pyproject.toml, GitHub Actions)
- **Manage CI/CD workflows** with configuration-driven GitHub Actions
- **Handle system dependencies** declaratively in pyproject.toml
- **Migrate legacy projects** from setup.cfg to modern formats
- **Debug CI failures** with automated diagnostics

## Installation

```bash
pip install wads          # light core: config reading + templating engine
pip install wads[create]  # full project-creation / publishing toolchain
pip install wads[all]     # create + docs
```

`wads` ships a **light core** (just enough to read `[tool.wads.ci]` /
`package.json` config and run the templating engine — handy in CI) plus a
heavier `create` extra (`requests`, `build`, `wheel`, `ruamel.yaml`) for
scaffolding and publishing. Use `wads[create]` (or `wads[all]`) when running
`populate`/`pack` to create or publish projects.

## Quick Start

### Create a New Project

```bash
populate my-project --root-url https://github.com/user/my-project
cd my-project
```

This creates a complete project structure with:

- `pyproject.toml` (modern build configuration)
- `README.md`, `LICENSE`, `.gitignore`
- Package directory with `__init__.py`
- GitHub Actions CI/CD workflow (optional)

### Add a frontend component for JS/TS parts (optional)

Python projects often ship a frontend component (a widget, a browser UI, a
TypeScript library). `populate --frontend <profile>` adds a parametrized
**NPM CI** alongside the Python one, following the same
“config-file-driven, fixed-workflow” model. Pick one or more **profiles**:

| Profile       | Adds                                                                   | Subdir   | CI                             |
|---------------|------------------------------------------------------------------------|----------|--------------------------------|
| `js`          | `package.json` (npm)                                                   | `js/`    | single-package `npm-ci.yml`    |
| `ts`          | `package.json` + `tsconfig.json` + `src/index.ts` (tsup build, vitest) | `ts/`    | single-package `npm-ci.yml`    |
| `ts-monorepo` | pnpm workspace root + `turbo.json` + an example `packages/core`        | `ts/`    | matrixed `npm-ci-monorepo.yml` |
```bash
# A single TypeScript component:
populate my-project --root-url https://github.com/user/my-project --frontend ts

# Several components at once — each in its own subdir, no workflow collision:
populate my-project --root-url https://github.com/user/my-project --frontend js,ts
```

`--with-npm` is kept as a back-compat alias for `--frontend js`.

Each component gets:

- a `package.json` with a namespaced `"wads"` config block (`wads.ci.*`)
  controlling node versions, lint/test/build commands, and publishing —
  analogous to `[tool.wads.ci]` in `pyproject.toml`;
- a path-filtered `.github/workflows/npm-ci[-<subdir>].yml` stub calling wads’s
  reusable NPM workflow (the `js` component keeps the bare `npm-ci.yml`; every
  other component gets `npm-ci-<subdir>.yml`, so multiple components never
  collide).

**Validation runs on every push/PR; publishing is opt-in.** It publishes only
when `wads.ci.publish.enabled` is `true` **and** the commit message contains
the marker **`[publish-npm]`** (deliberately distinct from the Python side).
Publishing uses npm OIDC trusted publishing + provenance by default (no
long-lived token). For a single component, customize the subdirectory and
package name with `--npm-subdir` / `--npm-package-name`.

**Package manager: npm or pnpm.** The single-package reusable workflow drives
**npm** by default and **pnpm** when selected — either explicitly via
`wads.ci.packageManager` (or `populate --npm-package-manager pnpm`) or
auto-detected from a `pnpm-lock.yaml` in the package directory. pnpm consumers
should declare a `"packageManager": "pnpm@x.y.z"` field in their `package.json`
(pnpm’s own convention); the CI reads the pnpm version from there. Existing npm
consumers are unaffected (no `pnpm-lock.yaml` → npm). The `ts-monorepo` profile
is pnpm-based by design.

The profile set is extensible: register your own with
`wads.profiles.register_frontend_profile(...)`.

### Configure CI in pyproject.toml

Edit your `pyproject.toml` to configure CI behavior:

```toml
[tool.wads.ci.testing]
python_versions = ["3.10", "3.12"]
pytest_args = ["-v", "--tb=short"]
coverage_enabled = true
test_on_windows = true

[tool.wads.ci.quality.ruff]
enabled = true

[tool.wads.ci.build]
sdist = true
wheel = true

# Opt-in licence gate over the installed dependency closure (default: off).
# See "Licence Perimeter" under CI Configuration Reference.
[tool.wads.licence]
enabled = false
```

The default `ci.yml` is a small stub that calls wads’s reusable workflow
(`i2mint/wads/.github/workflows/uv-ci.yml@master`); all behavior is driven by
`[tool.wads.ci.*]` above. Publishing to PyPI happens automatically on your
repo’s **default branch** — but only when the Linux test matrix passes.

### Configure Secrets (CI environment variables)

If your tests need API keys or other secrets, declare them once and let wads
wire up both the GitHub Actions transport and the job environment:

```bash
wads-secrets add OPENAI_API_KEY            # env var == GitHub secret name
wads-secrets add HF_TOKEN HF_WRITE_TOKEN    # env var <- differently-named secret
wads-secrets add DATABASE_URL --kind required   # fail CI if the secret is unset
wads-secrets add TEST_LEVEL --variable      # non-sensitive value -> repo variable
wads-secrets list                           # show what's configured
```

`wads-secrets add` (a) records the variable in `[tool.wads.ci.env]`, (b) adds its secret to the stub’s `secrets:` list, and (c) runs `gh secret set` (or `gh variable set` with `--variable`) if `gh` is installed (value taken from `$VAR_NAME` or `--value`). Under the hood there are two layers: a **transport**, where the stub passes secrets to the reusable workflow by name (`PYPI_PASSWORD` plus each declared one), and an **env policy** (`[tool.wads.ci.env]`: `required_envvars` / `test_envvars` / `extra_envvars` / `defaults` / `secret_aliases`) that decides which values become job env vars. Each declared name resolves against secrets first, then repository *variables* (the right home for non-sensitive values); committed constants can go straight into `[tool.wads.ci.env].defaults`. A `required` name that resolves to nothing fails the build; an undeclared secret is never written to the environment.

Named secrets must be in the superset in `wads.ci_secrets.DEFAULT_CI_SECRETS`, or GitHub rejects the workflow at parse time; `wads-secrets` and `wads-migrate` warn about such names, and the superset grows additively (adding a name never breaks a stub). The opt-in alternative, `wads-migrate ci-to-stub --transport json`, passes the repo’s whole secrets context as one `WADS_CI_SECRETS_JSON` secret, so any name works. It is not the default because it hands every secret the repo can read to the called workflow, and GitHub’s malicious-workflow scanner holds its runs: the run ends `action_required` with zero jobs and no log ([#74](https://github.com/i2mint/wads/issues/74), [#101](https://github.com/i2mint/wads/issues/101)). Re-rendering a stub (`wads-migrate ci-to-stub`, `ci-on-demand`, `fleet-stub`) writes the named transport, converting a JSON stub, unless `--transport json` is given or a declared name is outside the superset.

### Declare System Dependencies

Need ffmpeg, ODBC drivers, or other system packages in CI? Declare them in `pyproject.toml`:

```toml
[tool.wads.ops.ffmpeg]
description = "Multimedia framework for video/audio processing"
url = "https://ffmpeg.org/"

check.linux = "which ffmpeg"
check.macos = "which ffmpeg"

install.linux = "sudo apt-get install -y ffmpeg"
install.macos = "brew install ffmpeg"
install.windows = "choco install ffmpeg -y"

note = "Required for audio processing features"
```

The `install-system-deps` action in your CI workflow will automatically install these.

## Core Features

### 1. Project Setup (`populate`)

Create new Python projects with modern best practices:

```bash
# Basic usage
populate my-project

# With custom settings
populate my-project \
  --root-url https://github.com/myorg/my-project \
  --description "My awesome project" \
  --author "Your Name" \
  --license apache
```

**Options:**

- `--root-url`: GitHub repository URL
- `--description`: Project description
- `--author`: Author name
- `--license`: License type (mit, apache, bsd, etc.)
- `--keywords`: Comma-separated keywords
- `--overwrite`: Files to overwrite if they exist

**Tip:** Configure defaults in `wads_configs.json` or use `WADS_CONFIGS_FILE` environment variable to point to your custom config.

### 2. Package and Publish (`pack`)

Build and publish packages to PyPI:

```bash
# See current configuration
pack current-configs

# Increment version and publish
pack go .

# Or step-by-step
pack increment-configs-version
pack run-setup
pack twine-upload-dist
```

### 3. Migration Tools (`wads-migrate`)

Migrate legacy projects to modern format:

```bash
# Migrate setup.cfg to pyproject.toml
wads-migrate setup-to-pyproject setup.cfg -o pyproject.toml

# Migrate old CI workflow to new format
wads-migrate ci-old-to-new .github/workflows/old-ci.yml -o .github/workflows/ci.yml
```

**Python API:**

```python
from wads.migration import migrate_setuptools_to_hatching, migrate_github_ci_old_to_new

# From setup.cfg file
pyproject_content = migrate_setuptools_to_hatching("setup.cfg")

# From setup.cfg dict
config = {"metadata": {"name": "myproject", "version": "1.0.0"}}
pyproject_content = migrate_setuptools_to_hatching(config)

# Migrate CI workflow
new_ci = migrate_github_ci_old_to_new(".github/workflows/ci.yml")
```

### 4. CI Debugging (`wads-ci-debug`)

Diagnose and fix GitHub Actions CI failures:

```bash
# Analyze latest failure
wads-ci-debug myorg/myrepo

# Analyze specific run
wads-ci-debug myorg/myrepo --run-id 1234567890

# Generate fix instructions
wads-ci-debug myorg/myrepo --fix --local-repo .
```

The tool will:

- Fetch CI logs from GitHub
- Parse test failures and errors
- Identify root causes
- Generate fix instructions with file locations and suggested changes

## On-demand CI

For repos that should not run CI on every push (Actions minutes are free on public repos but metered on private ones), set:

```toml
[tool.wads.ci.trigger]
mode = "on-demand"          # default "auto": every push and PR, as before
run_ci_marker = "[run ci]"  # the default marker
```

**Nothing runs unless asked.** CI runs only when the commit *subject* (its first line) contains the marker, or when someone starts it by hand (`gh workflow run ci.yml --ref <branch>`, or the Actions tab), which is always allowed. Tests, publishing and GitHub Pages all obey the same gate.

```bash
git commit -m "Fix the parser [run ci]"   # runs CI
git commit -m "Fix the parser"            # runs nothing
```

A marker quoted only in a squash-merged PR body does not count. The stub’s zero-cost pre-filter lets that run start (one short setup job), and the reusable workflow checks the extracted subject before running anything else.

Worth knowing before you flip:

- **A manual run on the default branch releases** when publishing is enabled, exactly like a `[run ci]` push. To test without releasing, run it on another branch, or use `wads ci-local`.
- **The `[publish]` marker needs `[run ci]` in the same subject.** This applies to repos with publishing disabled, where `[publish]` forces a release.
- **Only the pushed head commit’s subject counts,** not earlier commits in the same push.
- **On-demand stubs drop the `pull_request` trigger.** A branch-protection rule that requires CI status checks will block PRs until those checks are removed from the rule.

Flip a repo in one idempotent command. It sets on-demand, `python_versions = ["3.12"]` and `test_on_windows = false`, and re-renders the stub, keeping its pin (its secrets move to the named transport):

```bash
wads-migrate ci-on-demand --dry-run   # show the diff, write nothing
wads-migrate ci-on-demand --commit    # apply and commit; then `git push`
```

Do locally what CI would have done, from the same `[tool.wads.ci]` config:

```bash
wads ci-local             # lint (ruff), tests (a fresh uv venv per python_versions entry), build
wads ci-local --dry-run   # print the plan, run nothing
wads ci-local --publish   # instead of the plain build: format, bump version (isee), build, PyPI upload, commit, tag, push
```

`--publish` refuses up front on a dirty tree, off the default branch, behind origin, or without a PyPI token (`$PYPI_PASSWORD`, then `$UV_PUBLISH_TOKEN`, then `~/.pypirc` `[pypi]` with `username = __token__`). `wads ci-local` needs [uv](https://docs.astral.sh/uv/getting-started/installation/) and git on `PATH`, and does not install `[tool.wads.ops.*]` system packages.

To go back, set `mode = "auto"` and run `wads-migrate ci-to-stub`. Workflow files other than `ci.yml` (cron jobs, Pages, npm) are not governed by the trigger; `ci-on-demand` lists any that still run unasked.

## CI Configuration Reference

Wads uses `pyproject.toml` as a single source of truth for CI configuration. Here’s what you can configure:

### Install Extras

New projects default to installing the `dev` extra in CI (`.[dev]`), matching
the `dev` extra the template declares under `[project.optional-dependencies]`
– so test-time tooling (pytest, ruff, …) actually gets installed instead of
silently never running. Override or opt out explicitly:

```toml
[tool.wads.ci.install]
extras = "dev"          # or a list, e.g. ["dev", "test"]; "" installs core deps only
```

### Python Versions and Testing

```toml
[tool.wads.ci.testing]
python_versions = ["3.10", "3.11", "3.12"]  # Test matrix
pytest_args = ["-v", "--tb=short"]           # Pytest arguments
coverage_enabled = true                      # Enable coverage
coverage_threshold = 80                      # Minimum coverage %
exclude_paths = ["examples", "scrap"]        # Paths to exclude
test_on_windows = true                       # Run Windows tests
windows_blocking = false                     # true = a failing Windows leg reddens the run
```

`windows_blocking` defaults to `false`: the Windows job is informational
(`continue-on-error`), which is how Windows-only defects — backslash path
separators, locale-decoded `read_text()` — merge behind a green tick. Set it to
`true` to make the run red. It reddens the **run**, not the release: `publish`
does not depend on the Windows job.

This knob only reaches the uv-based CI (`github_ci_uv.yml` / the reusable
`uv-ci.yml` workflow, the default for new projects). The legacy
`github_ci_publish_2025.yml` template and its `actions/windows-tests` action
still hardcode `continue-on-error: true` unconditionally — a failing Windows
leg there stays informational regardless of this setting. Repos on the legacy
template should migrate to the uv stub (`wads-migrate ci-to-stub`) to pick it
up (see i2mint/wads#90).

### Code Quality Tools

```toml
[tool.wads.ci.quality.ruff]
enabled = true
# line_length = 88

[tool.wads.ci.quality.mypy]
enabled = false
# strict = true
```

### Custom Commands

```toml
[tool.wads.ci.commands]
pre_test = [
    "python scripts/setup_test_data.py",
]
post_test = [
    "python scripts/cleanup.py",
]
```

### Build and Publish

```toml
[tool.wads.ci.build]
sdist = true
wheel = true

[tool.wads.ci.publish]
enabled = true  # Publish to PyPI on main/master
```

### Licence Perimeter (`wads-licence-check`)

Fails the build when the **installed dependency closure** carries a licence the
project’s policy forbids — copyleft (GPL / AGPL / LGPL), or source-available /
non-commercial (SSPL, BUSL, Elastic-2.0, RAIL, CC-BY-NC).

It walks the *transitive* closure, not just the declared list, because that is
where the exposures actually hide, and it reads a distribution’s declaration
through a **precision ladder** — PEP 639 `License-Expression`, then the
`License ::` trove classifiers, then the **first line** of the free-text
`License` field. No single field is enough: `click` declares an expression and
no classifiers, `i2` declares neither and only a free-text field, and a
whole-field substring scan flags BSD-3-Clause numpy as copyleft because its
field carries an LGPL URL for a vendored notice.

Run it anywhere:

```bash
wads-licence-check                                    # this project
wads-licence-check path/to/project --json             # for a fleet sweep
wads-licence-check . --python .venv/bin/python        # read another env
```

The CI gate is **opt-in**. A repo that declares nothing sees no change:

```toml
[tool.wads.licence]
enabled = true                    # default false: opt in per repo
include-extras = []               # [] = hard deps only; ["*"] = every extra
unknown-is-failure = true         # a blank licence field is *unaudited*, not fine
unclassified-is-failure = false   # e.g. MPL-2.0: reported, does not fail

[tool.wads.licence.exceptions]
certifi = "MPL-2.0 — weak, file-level, over an unmodified CA bundle. Audited 2026-08."
```

That is the whole configuration most repos need. The `allowed` / `forbidden`
pattern lists are deliberately absent from it: **they REPLACE the defaults, they
do not extend them**, so writing them out by hand is a narrowing unless the list
is a superset of what ships. If you do set them, write them as TOML **literal**
strings — single quotes — because a basic string processes escapes and turns
`"\bGPL"` into a backspace character followed by `GPL`, which matches nothing:

```toml
[tool.wads.licence]
# Start from wads.licence_check.DFLT_FORBIDDEN and ADD, rather than replacing:
forbidden = [
  '\bAGPL',
  '\bAffero\b',
  '\bGPL(?![\w.+-]*\s+with\b)',
  '\bGNU General Public\b',
  '\bLGPL',
  '\bLesser General Public\b',
  '\bLibrary General Public\b',
  '\bNethack General Public\b',
  '\bEUPL\b',
  '\bBusiness Source\b',
  '\bBUSL\b',
  '\bSSPL\b',
  '\bElastic[- ]?(2\.0|License|v2)\b',
  '(?:\b|-)(?:open)?rail(?:-m)?\b',
  '\bCC[- ]BY[- ]NC\b',
  '\bNon[- ]?Commercial\b',
  '\bProprietary\b',
  '\bYourOwnAddition\b',   # the point: ADD to the defaults, never restate a subset
]
```

Exceptions take either the terse map above or an array-of-tables, which is the
shape to reach for when the decision needs a record behind it:

```toml
[[tool.wads.licence.exceptions]]
dependency = "PyGithub"
licence = "LGPL (classifier only; no SPDX expression published)"
scope = "core"
decided = "2026-08-30"
decided_in = "https://github.com/thorwhalen/hubcap/issues/10"
reason = """
Accepted, not removable: PyGithub's objects are this package's values, so there
is no honest "core without it" to install. Consumed by ordinary import, neither
vendored nor patched, so the LGPL relink freedom is intact.
"""
```

Note the table lives under `[tool.wads]`, not `[tool.wads.ci]`: the policy is a
fact about the package, and the tool is useful outside CI. Only `enabled` is a
CI concern.

Exit codes are `0` (holds), `1` (breached) and `2` (the tool could not run —
bad config, unreadable environment, a policy that cannot detect).

#### The self-check

Every run first proves the live policy still catches known-copyleft
declarations *and* still clears known-permissive ones, and refuses to report at
all if it cannot — a detector nobody has demonstrated is a detector nobody has
checked.

The bar is per licence FAMILY, caught whole or not at all. Permitting a family
outright is a coherent stance (LGPL for dynamically linked libraries is the
usual one) and stays expressible; it is then named in every run’s output, so
“PERIMETER HOLDS” is never read as “there is no LGPL in here”. Catching *part*
of a family is refused, because it is never a stance — it is the bug. A gate
whose LGPL pattern ended in `\b` caught the legacy `GNU Library or Lesser`
classifier and missed `LGPLv2`, `LGPLv2+`, `LGPLv3` and `LGPLv3+`, i.e. every
modern spelling, while reporting itself as working.

#### What counts as a failure

`forbidden` and a blank declaration (`unknown-is-failure`, default on) fail, and
so does a **declared dependency that is not installed** in the environment being
read: that is a piece of the perimeter nobody looked at, and it is the same
confident-green failure as reading the wrong environment. The one exception is a
requirement gated on an environment marker (`tomli; python_version < "3.11"`),
which is reported as `NOT APPLICABLE` and does not fail.

For the same reason the tool refuses to run at all on a project whose
`[project].dependencies` is absent or listed in `dynamic` — an empty closure it
could not read is not an empty closure. Write `dependencies = []` if a project
genuinely has none.

## System Dependencies

System dependencies are declared using the `[tool.wads.ops.*]` format and automatically installed in CI via the `install-system-deps` action.

**Format:**

```toml
[tool.wads.ops.{package-name}]
description = "Description of the package"
url = "https://package-homepage.com"

# Check if already installed (exit code 0 = present)
check.linux = "which package-name"
check.macos = "brew list package-name"
check.windows = "where package-name"

# Install commands (string or list of strings)
install.linux = "sudo apt-get install -y package-name"
install.macos = "brew install package-name"
install.windows = "choco install package-name -y"

# Optional metadata
note = "Additional installation notes"
alternatives = ["alternative-package"]
```

**Real-world example (ODBC drivers):**

```toml
[tool.wads.ops.unixodbc]
description = "ODBC driver interface for database connectivity"
url = "https://www.unixodbc.org/"

check.linux = "dpkg -s unixodbc || rpm -q unixODBC"
check.macos = "brew list unixodbc"

install.linux = [
    "sudo apt-get update",
    "sudo apt-get install -y unixodbc unixodbc-dev"
]
install.macos = "brew install unixodbc"

note = "On Alpine: apk add unixodbc unixodbc-dev"
alternatives = ["iodbc"]
```

See [misc/docs/SYSTEM_DEPENDENCIES.md]() for comprehensive examples.

## Documentation

- **[System Dependencies Guide]()** - `[tool.wads.ops.*]` format and examples
- **[Migration Guide]()** - Migrate from setup.cfg to pyproject.toml
- **[Utilities Reference]()** - CLI tools (`wads-ci-debug`, `wads-migrate`)
- **[.claude/CLAUDE.md]()** - AI agent guide for working with this project

## Troubleshooting

### Version Tag Misalignment

If PyPI publishing fails with “appears to already exist”:

```default
WARNING  Skipping mypackage-0.1.4-py3-none-any.whl because it appears to already exist
```

This means your git tags are misaligned with the version in `pyproject.toml`.

**Fix:**

1. Check the current PyPI version: https://pypi.org/project/your-package/
2. Update `version` in `pyproject.toml` to a higher number
3. Create and push git tag:
   ```bash
   git tag 0.1.5
   git push origin 0.1.5
   ```

### CI Failures

Use `wads-ci-debug` to analyze failures:

```bash
wads-ci-debug myorg/myrepo --fix
```

Common issues:

- Missing system dependencies → Add to `[tool.wads.ops.*]`
- Python version incompatibilities → Check `python_versions` in `[tool.wads.ci.testing]`
- Test failures → Review generated fix instructions

## For carbon-based contributors

Everything above is for users of wads, human or not. This part is for working on wads itself.

**Dev setup.** The test suite scaffolds and builds packages, so it needs the `create` extra:

```bash
uv venv && . .venv/bin/activate
uv pip install -e ".[create,docs,skills,test]"
```

**Run the tests the way CI does.** CI calls pytest with no path, so `testpaths = ["wads"]` collects both `wads/tests` and every doctest in the package:

```bash
python -m pytest --doctest-modules -o doctest_optionflags='ELLIPSIS IGNORE_EXCEPTION_DETAIL' --ignore=examples --ignore=scrap
```

CI tests Python 3.10 and 3.12; run 3.11 as well before merging, because a dataclass default once broke there and nowhere else ([#100](https://github.com/i2mint/wads/issues/100)).

**Build the docs** with `pip install -e ".[docs]"` and then `epythet build`.

**Why it is built this way.** wads is a foundation package. Its reusable workflow and actions run from `@master` in every wads-managed repo, and every merge to `master` publishes a release to PyPI. So changes to `.github/workflows/uv-ci.yml`, `actions/*` or the stub template are fleet-wide changes: they come with tests (the push-back script, for example, is exercised against real throwaway git repos), and the populate output is pinned by golden files. [`.claude/CLAUDE.md`]() has the design rationale, and the `wads-dev-workflow` skill has the maintainer checklist.

**Contributing and questions.** Open an issue or a pull request at [github.com/i2mint/wads](https://github.com/i2mint/wads/issues).

## License

Apache Software License 2.0

## Links

- **PyPI:** https://pypi.org/project/wads/
- **GitHub:** https://github.com/i2mint/wads
- **Issues:** https://github.com/i2mint/wads/issues

<p class="epythet-aggregates">This documentation as a single file: <a href="wads.md">wads.md</a> (Markdown, for agents).</p>


# _autosummary/wads.agents.ci_debug_agent.html.md

# wads.agents.ci_debug_agent

CI Debugging Agent for Wads

This agent analyzes failed CI runs, diagnoses issues, and proposes fixes.
It can:

- Fetch GitHub Actions logs via API
- Parse test failures and error messages
- Analyze code context
- Diagnose root causes (missing deps, config issues, code bugs)
- Propose fixes (install commands, code changes, config updates)

Usage:

```default
python -m wads.ci_debug_agent <repo> [--run-id RUN_ID] [--fix]
```

### Functions

| [`diagnose_ci_failure`](_autosummary/wads.agents.ci_debug_agent.html.md#wads.agents.ci_debug_agent.diagnose_ci_failure)(repo[, run_id])             | Diagnose a CI failure.                                    |
|--------------------------------------------------------------------------------------------------|-----------------------------------------------------------|
| [`diagnose_missing_python_deps`](_autosummary/wads.agents.ci_debug_agent.html.md#wads.agents.ci_debug_agent.diagnose_missing_python_deps)(failures)          | Identify missing Python dependencies from import errors.  |
| [`diagnose_missing_system_deps`](_autosummary/wads.agents.ci_debug_agent.html.md#wads.agents.ci_debug_agent.diagnose_missing_system_deps)(failures, logs)    | Identify missing system dependencies from error messages. |
| [`fetch_workflow_logs`](_autosummary/wads.agents.ci_debug_agent.html.md#wads.agents.ci_debug_agent.fetch_workflow_logs)(repo, run_id)               | Fetch logs for a specific workflow run.                   |
| [`fetch_workflow_runs`](_autosummary/wads.agents.ci_debug_agent.html.md#wads.agents.ci_debug_agent.fetch_workflow_runs)(repo[, limit])              | Fetch recent workflow runs for a repository.              |
| [`generate_fix_instructions`](_autosummary/wads.agents.ci_debug_agent.html.md#wads.agents.ci_debug_agent.generate_fix_instructions)(diagnosis, repo_path) | Generate detailed fix instructions.                       |
| [`get_github_token`](_autosummary/wads.agents.ci_debug_agent.html.md#wads.agents.ci_debug_agent.get_github_token)()                              | Get GitHub token from environment.                        |
| [`main`](_autosummary/wads.agents.ci_debug_agent.html.md#wads.agents.ci_debug_agent.main)()                                          | CLI entry point for wads CI debug agent.                  |
| [`parse_ci_warnings`](_autosummary/wads.agents.ci_debug_agent.html.md#wads.agents.ci_debug_agent.parse_ci_warnings)(logs)                         | Parse CI warnings from logs that may indicate issues.     |
| [`parse_pytest_failures`](_autosummary/wads.agents.ci_debug_agent.html.md#wads.agents.ci_debug_agent.parse_pytest_failures)(logs)                     | Parse pytest failures and collection errors from CI logs. |
| [`print_diagnosis`](_autosummary/wads.agents.ci_debug_agent.html.md#wads.agents.ci_debug_agent.print_diagnosis)(diagnosis)                      | Print formatted diagnosis report.                         |

### Classes

| [`CIDiagnosis`](_autosummary/wads.agents.ci_debug_agent.html.md#wads.agents.ci_debug_agent.CIDiagnosis)(failures, missing_system_deps, ...)   | Result of CI failure diagnosis.   |
|----------------------------------------------------------------------------------------------------|-----------------------------------|
| [`TestFailure`](_autosummary/wads.agents.ci_debug_agent.html.md#wads.agents.ci_debug_agent.TestFailure)(test_name, error_type, ...[, ...])    | Represents a single test failure. |

### *class* wads.agents.ci_debug_agent.CIDiagnosis(failures, missing_system_deps, missing_python_deps, config_issues, proposed_fixes, confidence, warnings=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Result of CI failure diagnosis.

### *class* wads.agents.ci_debug_agent.TestFailure(test_name, error_type, error_message, traceback, file_path=None, line_number=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Represents a single test failure.

### wads.agents.ci_debug_agent.diagnose_ci_failure(repo, run_id=None)

Diagnose a CI failure.

* **Parameters:**
  * **repo** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Repository in format ‘owner/name’
  * **run_id** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`int`](https://docs.python.org/3/builtins/functions.html#int)]) – Specific run ID, or None for latest failed run
* **Return type:**
  [`CIDiagnosis`](_autosummary/wads.agents.ci_debug_agent.html.md#wads.agents.ci_debug_agent.CIDiagnosis)
* **Returns:**
  CIDiagnosis with analysis results

### wads.agents.ci_debug_agent.diagnose_missing_python_deps(failures)

Identify missing Python dependencies from import errors.

* **Parameters:**
  **failures** ([`List`](https://docs.python.org/3/library/typing.html#typing.List)[[`TestFailure`](_autosummary/wads.agents.ci_debug_agent.html.md#wads.agents.ci_debug_agent.TestFailure)]) – List of test failures
* **Return type:**
  [`List`](https://docs.python.org/3/library/typing.html#typing.List)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]
* **Returns:**
  List of likely missing Python packages

### wads.agents.ci_debug_agent.diagnose_missing_system_deps(failures, logs)

Identify missing system dependencies from error messages.

* **Parameters:**
  * **failures** ([`List`](https://docs.python.org/3/library/typing.html#typing.List)[[`TestFailure`](_autosummary/wads.agents.ci_debug_agent.html.md#wads.agents.ci_debug_agent.TestFailure)]) – List of test failures
  * **logs** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Full CI logs
* **Return type:**
  [`List`](https://docs.python.org/3/library/typing.html#typing.List)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]
* **Returns:**
  List of likely missing system dependencies

### wads.agents.ci_debug_agent.fetch_workflow_logs(repo, run_id)

Fetch logs for a specific workflow run.

* **Parameters:**
  * **repo** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Repository in format ‘owner/name’
  * **run_id** ([`int`](https://docs.python.org/3/builtins/functions.html#int)) – Workflow run ID
* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)
* **Returns:**
  Log content as string

### wads.agents.ci_debug_agent.fetch_workflow_runs(repo, limit=5)

Fetch recent workflow runs for a repository.

* **Parameters:**
  * **repo** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Repository in format ‘owner/name’
  * **limit** ([`int`](https://docs.python.org/3/builtins/functions.html#int)) – Number of runs to fetch
* **Return type:**
  [`List`](https://docs.python.org/3/library/typing.html#typing.List)[[`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)]
* **Returns:**
  List of workflow run dictionaries

### wads.agents.ci_debug_agent.generate_fix_instructions(diagnosis, repo_path)

Generate detailed fix instructions.

* **Parameters:**
  * **diagnosis** ([`CIDiagnosis`](_autosummary/wads.agents.ci_debug_agent.html.md#wads.agents.ci_debug_agent.CIDiagnosis)) – CIDiagnosis result
  * **repo_path** ([`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)) – Local path to repository
* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)
* **Returns:**
  Formatted fix instructions

### wads.agents.ci_debug_agent.get_github_token()

Get GitHub token from environment.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### wads.agents.ci_debug_agent.main()

CLI entry point for wads CI debug agent.

### wads.agents.ci_debug_agent.parse_ci_warnings(logs)

Parse CI warnings from logs that may indicate issues.

* **Parameters:**
  **logs** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Full CI logs
* **Return type:**
  [`List`](https://docs.python.org/3/library/typing.html#typing.List)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]
* **Returns:**
  List of warning messages

### wads.agents.ci_debug_agent.parse_pytest_failures(logs)

Parse pytest failures and collection errors from CI logs.

* **Parameters:**
  **logs** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – CI log content
* **Return type:**
  [`List`](https://docs.python.org/3/library/typing.html#typing.List)[[`TestFailure`](_autosummary/wads.agents.ci_debug_agent.html.md#wads.agents.ci_debug_agent.TestFailure)]
* **Returns:**
  List of TestFailure objects

### wads.agents.ci_debug_agent.print_diagnosis(diagnosis)

Print formatted diagnosis report.


# _autosummary/wads.agents.dependency_resolver.html.md

# wads.agents.dependency_resolver

Dependency Resolver Agent

Automatically analyzes import errors, missing dependencies, and suggests fixes.
Can scan local code, analyze error messages, and propose dependency additions.

### Functions

| [`analyze_dependencies`](_autosummary/wads.agents.dependency_resolver.html.md#wads.agents.dependency_resolver.analyze_dependencies)(project_path[, ...])   | Analyze project dependencies and identify issues.     |
|----------------------------------------------------------------------------------------------|-------------------------------------------------------|
| [`extract_imports_from_file`](_autosummary/wads.agents.dependency_resolver.html.md#wads.agents.dependency_resolver.extract_imports_from_file)(file_path)        | Extract all import statements from a Python file.     |
| [`get_installed_packages`](_autosummary/wads.agents.dependency_resolver.html.md#wads.agents.dependency_resolver.get_installed_packages)()                    | Get list of installed packages using pip.             |
| [`main`](_autosummary/wads.agents.dependency_resolver.html.md#wads.agents.dependency_resolver.main)()                                      | CLI entry point for dependency resolver.              |
| [`parse_error_message`](_autosummary/wads.agents.dependency_resolver.html.md#wads.agents.dependency_resolver.parse_error_message)(error_msg)              | Parse error messages to extract missing dependencies. |
| [`print_report`](_autosummary/wads.agents.dependency_resolver.html.md#wads.agents.dependency_resolver.print_report)(report)                        | Print formatted dependency report.                    |
| [`read_project_dependencies`](_autosummary/wads.agents.dependency_resolver.html.md#wads.agents.dependency_resolver.read_project_dependencies)(pyproject_path)   | Read declared dependencies from pyproject.toml.       |
| [`resolve_package_name`](_autosummary/wads.agents.dependency_resolver.html.md#wads.agents.dependency_resolver.resolve_package_name)(import_name)           | Resolve import name to package name.                  |
| [`scan_project_imports`](_autosummary/wads.agents.dependency_resolver.html.md#wads.agents.dependency_resolver.scan_project_imports)(project_path[, ...])   | Scan all Python files in a project for imports.       |

### Classes

| [`DependencyIssue`](_autosummary/wads.agents.dependency_resolver.html.md#wads.agents.dependency_resolver.DependencyIssue)(package_name, import_statement)   | Represents a missing or problematic dependency.   |
|----------------------------------------------------------------------------------------------------|---------------------------------------------------|
| [`DependencyReport`](_autosummary/wads.agents.dependency_resolver.html.md#wads.agents.dependency_resolver.DependencyReport)(missing_packages, ...)           | Result of dependency analysis.                    |

### *class* wads.agents.dependency_resolver.DependencyIssue(package_name, import_statement, file_path=None, line_number=None, suggested_package=None, error_message=None, is_installed=False)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Represents a missing or problematic dependency.

### *class* wads.agents.dependency_resolver.DependencyReport(missing_packages, unused_packages, version_conflicts, recommendations)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Result of dependency analysis.

### wads.agents.dependency_resolver.analyze_dependencies(project_path, error_logs=None, check_unused=True)

Analyze project dependencies and identify issues.

* **Parameters:**
  * **project_path** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)) – Path to project directory
  * **error_logs** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – Optional error logs to parse
  * **check_unused** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Whether to check for unused dependencies
* **Return type:**
  [`DependencyReport`](_autosummary/wads.agents.dependency_resolver.html.md#wads.agents.dependency_resolver.DependencyReport)
* **Returns:**
  DependencyReport with analysis results

### wads.agents.dependency_resolver.extract_imports_from_file(file_path)

Extract all import statements from a Python file.

* **Parameters:**
  **file_path** ([`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)) – Path to Python file
* **Return type:**
  [`Set`](https://docs.python.org/3/library/typing.html#typing.Set)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]
* **Returns:**
  Set of imported module names

### wads.agents.dependency_resolver.get_installed_packages()

Get list of installed packages using pip.

* **Return type:**
  [`Set`](https://docs.python.org/3/library/typing.html#typing.Set)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]
* **Returns:**
  Set of installed package names (lowercase)

### wads.agents.dependency_resolver.main()

CLI entry point for dependency resolver.

### wads.agents.dependency_resolver.parse_error_message(error_msg)

Parse error messages to extract missing dependencies.

* **Parameters:**
  **error_msg** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Error message from Python/pytest
* **Return type:**
  [`List`](https://docs.python.org/3/library/typing.html#typing.List)[[`DependencyIssue`](_autosummary/wads.agents.dependency_resolver.html.md#wads.agents.dependency_resolver.DependencyIssue)]
* **Returns:**
  List of DependencyIssue objects

### wads.agents.dependency_resolver.print_report(report)

Print formatted dependency report.

### wads.agents.dependency_resolver.read_project_dependencies(pyproject_path)

Read declared dependencies from pyproject.toml.

* **Parameters:**
  **pyproject_path** ([`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)) – Path to pyproject.toml
* **Return type:**
  [`Tuple`](https://docs.python.org/3/library/typing.html#typing.Tuple)[[`Set`](https://docs.python.org/3/library/typing.html#typing.Set)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`Set`](https://docs.python.org/3/library/typing.html#typing.Set)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]
* **Returns:**
  Tuple of (main_dependencies, dev_dependencies)

### wads.agents.dependency_resolver.resolve_package_name(import_name)

Resolve import name to package name.

* **Parameters:**
  **import_name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Name used in import statement
* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)
* **Returns:**
  Likely package name for pip install

### wads.agents.dependency_resolver.scan_project_imports(project_path, exclude_patterns=None)

Scan all Python files in a project for imports.

* **Parameters:**
  * **project_path** ([`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)) – Root path of project
  * **exclude_patterns** ([`List`](https://docs.python.org/3/library/typing.html#typing.List)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – Patterns to exclude (e.g., ‘tests’, ‘venv’)
* **Return type:**
  [`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Set`](https://docs.python.org/3/library/typing.html#typing.Set)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]
* **Returns:**
  Dict mapping file paths to sets of imported modules


# _autosummary/wads.agents.html.md

# wads.agents

Wads AI Agents

Autonomous agents for diagnosing and fixing common development issues.

Available Agents:

- ci_debug_agent: Analyzes failed CI runs and proposes fixes
- dependency_resolver: Analyzes import errors and missing dependencies
- test_analyzer: Analyzes pytest failures and categorizes them

### Functions

| [`diagnose_ci_failure`](_autosummary/wads.agents.html.md#wads.agents.diagnose_ci_failure)(repo[, run_id])       | Diagnose a CI failure.                            |
|--------------------------------------------------------------------------------------------|---------------------------------------------------|
| [`analyze_dependencies`](_autosummary/wads.agents.html.md#wads.agents.analyze_dependencies)(project_path[, ...]) | Analyze project dependencies and identify issues. |
| [`parse_pytest_output`](_autosummary/wads.agents.html.md#wads.agents.parse_pytest_output)(output)               | Parse pytest output and analyze failures.         |

### Classes

| [`CIDiagnosis`](_autosummary/wads.agents.html.md#wads.agents.CIDiagnosis)(failures, missing_system_deps, ...)   | Result of CI failure diagnosis.                 |
|----------------------------------------------------------------------------------------------------|-------------------------------------------------|
| [`TestFailure`](_autosummary/wads.agents.html.md#wads.agents.TestFailure)(test_name, error_type, ...[, ...])    | Represents a single test failure.               |
| [`DependencyReport`](_autosummary/wads.agents.html.md#wads.agents.DependencyReport)(missing_packages, ...)           | Result of dependency analysis.                  |
| [`DependencyIssue`](_autosummary/wads.agents.html.md#wads.agents.DependencyIssue)(package_name, import_statement)   | Represents a missing or problematic dependency. |
| [`TestAnalysisReport`](_autosummary/wads.agents.html.md#wads.agents.TestAnalysisReport)(total_failures, ...)           | Result of test failure analysis.                |
| [`TestFailurePattern`](_autosummary/wads.agents.html.md#wads.agents.TestFailurePattern)(pattern_type, count, ...)      | Represents a pattern of test failures.          |

### *class* wads.agents.CIDiagnosis(failures, missing_system_deps, missing_python_deps, config_issues, proposed_fixes, confidence, warnings=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Result of CI failure diagnosis.

### *class* wads.agents.DependencyIssue(package_name, import_statement, file_path=None, line_number=None, suggested_package=None, error_message=None, is_installed=False)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Represents a missing or problematic dependency.

### *class* wads.agents.DependencyReport(missing_packages, unused_packages, version_conflicts, recommendations)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Result of dependency analysis.

### *class* wads.agents.TestAnalysisReport(total_failures, total_errors, failure_patterns, flaky_tests, slow_tests, recommendations)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Result of test failure analysis.

### *class* wads.agents.TestFailure(test_name, error_type, error_message, traceback, file_path=None, line_number=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Represents a single test failure.

### *class* wads.agents.TestFailurePattern(pattern_type, count, examples, suggested_fix, severity)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Represents a pattern of test failures.

### wads.agents.analyze_dependencies(project_path, error_logs=None, check_unused=True)

Analyze project dependencies and identify issues.

* **Parameters:**
  * **project_path** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)) – Path to project directory
  * **error_logs** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – Optional error logs to parse
  * **check_unused** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Whether to check for unused dependencies
* **Return type:**
  [`DependencyReport`](_autosummary/wads.agents.dependency_resolver.html.md#wads.agents.dependency_resolver.DependencyReport)
* **Returns:**
  DependencyReport with analysis results

### wads.agents.diagnose_ci_failure(repo, run_id=None)

Diagnose a CI failure.

* **Parameters:**
  * **repo** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Repository in format ‘owner/name’
  * **run_id** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`int`](https://docs.python.org/3/builtins/functions.html#int)]) – Specific run ID, or None for latest failed run
* **Return type:**
  [`CIDiagnosis`](_autosummary/wads.agents.ci_debug_agent.html.md#wads.agents.ci_debug_agent.CIDiagnosis)
* **Returns:**
  CIDiagnosis with analysis results

### wads.agents.parse_pytest_output(output)

Parse pytest output and analyze failures.

* **Parameters:**
  **output** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Pytest output text
* **Return type:**
  [`TestAnalysisReport`](_autosummary/wads.agents.test_analyzer.html.md#wads.agents.test_analyzer.TestAnalysisReport)
* **Returns:**
  TestAnalysisReport with analysis

### Modules

| [`ci_debug_agent`](_autosummary/wads.agents.ci_debug_agent.html.md#module-wads.agents.ci_debug_agent)           | CI Debugging Agent for Wads   |
|-------------------------------------------------------------------------------------------------------------|-------------------------------|
| [`dependency_resolver`](_autosummary/wads.agents.dependency_resolver.html.md#module-wads.agents.dependency_resolver) | Dependency Resolver Agent     |
| [`test_analyzer`](_autosummary/wads.agents.test_analyzer.html.md#module-wads.agents.test_analyzer)             | Test Failure Analyzer Agent   |


# _autosummary/wads.agents.test_analyzer.html.md

# wads.agents.test_analyzer

Test Failure Analyzer Agent

Analyzes pytest test failures, categorizes them, and suggests fixes.
Identifies patterns in failures and provides actionable recommendations.

### Functions

| [`categorize_failure`](_autosummary/wads.agents.test_analyzer.html.md#wads.agents.test_analyzer.categorize_failure)(error_type, error_message)   | Categorize test failure type.             |
|--------------------------------------------------------------------------------------------------|-------------------------------------------|
| [`main`](_autosummary/wads.agents.test_analyzer.html.md#wads.agents.test_analyzer.main)()                                          | CLI entry point for test analyzer.        |
| [`parse_pytest_output`](_autosummary/wads.agents.test_analyzer.html.md#wads.agents.test_analyzer.parse_pytest_output)(output)                     | Parse pytest output and analyze failures. |
| [`print_report`](_autosummary/wads.agents.test_analyzer.html.md#wads.agents.test_analyzer.print_report)(report)                            | Print formatted test analysis report.     |
| [`suggest_fix`](_autosummary/wads.agents.test_analyzer.html.md#wads.agents.test_analyzer.suggest_fix)(category, error_message)            | Suggest fix based on failure category.    |

### Classes

| [`TestAnalysisReport`](_autosummary/wads.agents.test_analyzer.html.md#wads.agents.test_analyzer.TestAnalysisReport)(total_failures, ...)      | Result of test failure analysis.       |
|-----------------------------------------------------------------------------------------------|----------------------------------------|
| [`TestFailurePattern`](_autosummary/wads.agents.test_analyzer.html.md#wads.agents.test_analyzer.TestFailurePattern)(pattern_type, count, ...) | Represents a pattern of test failures. |

### *class* wads.agents.test_analyzer.TestAnalysisReport(total_failures, total_errors, failure_patterns, flaky_tests, slow_tests, recommendations)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Result of test failure analysis.

### *class* wads.agents.test_analyzer.TestFailurePattern(pattern_type, count, examples, suggested_fix, severity)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Represents a pattern of test failures.

### wads.agents.test_analyzer.categorize_failure(error_type, error_message)

Categorize test failure type.

* **Parameters:**
  * **error_type** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Type of error (AssertionError, ValueError, etc.)
  * **error_message** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Error message
* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)
* **Returns:**
  Category string

### wads.agents.test_analyzer.main()

CLI entry point for test analyzer.

### wads.agents.test_analyzer.parse_pytest_output(output)

Parse pytest output and analyze failures.

* **Parameters:**
  **output** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Pytest output text
* **Return type:**
  [`TestAnalysisReport`](_autosummary/wads.agents.test_analyzer.html.md#wads.agents.test_analyzer.TestAnalysisReport)
* **Returns:**
  TestAnalysisReport with analysis

### wads.agents.test_analyzer.print_report(report)

Print formatted test analysis report.

### wads.agents.test_analyzer.suggest_fix(category, error_message)

Suggest fix based on failure category.

* **Parameters:**
  * **category** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Failure category
  * **error_message** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Error message
* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)
* **Returns:**
  Suggested fix description


# _autosummary/wads.ci_config.html.md

# wads.ci_config

Utilities for reading and applying CI configuration from pyproject.toml.

This module provides the infrastructure for using pyproject.toml as the single
source of truth for CI configuration, eliminating hardcoded project-specific
settings in CI workflow files.

### Functions

| [`get_ci_config_or_defaults`](_autosummary/wads.ci_config.html.md#wads.ci_config.get_ci_config_or_defaults)(pyproject_path[, ...])   | Read CI configuration from pyproject.toml, using defaults if file doesn't exist.                                                                                                                                 |
|-----------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`read_ci_config`](_autosummary/wads.ci_config.html.md#wads.ci_config.read_ci_config)(pyproject_path)                     | Read CI configuration from pyproject.toml.                                                                                                                                                                       |
| [`render_minimal_env_placeholders`](_autosummary/wads.ci_config.html.md#wads.ci_config.render_minimal_env_placeholders)(...)               | Render the inline template's env placeholders without a [tool.wads.ci] config: workflow-level env gets PROJECT_NAME only, and the test-job placeholders render empty (there are no declared secret-backed vars). |

### Classes

| [`CIConfig`](_autosummary/wads.ci_config.html.md#wads.ci_config.CIConfig)(pyproject_data[, project_name])   | Represents CI configuration extracted from pyproject.toml.   |
|---------------------------------------------------------------------------------------------|--------------------------------------------------------------|

### *class* wads.ci_config.CIConfig(pyproject_data, project_name=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Represents CI configuration extracted from pyproject.toml.

#### CI_PROVIDED_PACKAGES *= ('pytest', 'pytest-cov', 'coverage', 'ruff')*

run-tests-uv installs pytest and
pytest-cov (which pulls coverage); ruff comes from the ruff actions.

* **Type:**
  Packages CI provides without any extra

#### TEST_EXTRA_NAMES *= ('dev', 'test', 'tests', 'testing', 'ci')*

Extra names that conventionally hold test-time tooling (i2mint/wads#59).

#### *property* build_config *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)*

Get build configuration.

#### *property* build_sdist *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

Check if source distribution should be built.

#### *property* build_wheel *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

Check if wheel should be built.

#### *property* commands_format *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]*

Get format commands.

#### *property* commands_lint *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]*

Get lint commands.

#### *property* commands_post_test *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]*

Get post-test commands.

#### *property* commands_pre_test *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]*

Get pre-test setup commands.

#### *property* commands_test *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]*

Get test commands (defaults to [‘pytest’]).

#### *property* coverage_enabled *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

Check if coverage is enabled.

#### *property* coverage_threshold *: [int](https://docs.python.org/3/builtins/functions.html#int)*

Get minimum coverage threshold (0 = no enforcement).

#### *property* docs_builder *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

Get documentation builder name.

#### *property* docs_config *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)*

Get documentation configuration.

#### *property* docs_enabled *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

Check if documentation generation is enabled.

#### *property* docs_ignore_paths *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]*

Get paths to ignore during documentation generation.

#### *property* env_secret_aliases *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [str](https://docs.python.org/3/builtins/stdtypes.html#str)]*

Map of env-var name -> backing GitHub *secret* name.

Lets a repo expose a secret under a different env-var name in CI, e.g.
read `secrets.HF_WRITE_TOKEN` but expose it to tests as `HF_TOKEN`:

```default
[tool.wads.ci.env.secret_aliases]
HF_TOKEN = "HF_WRITE_TOKEN"
```

Env-var names not listed here are backed by an identically-named secret.

#### *property* env_vars_all *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]*

Get all environment variable names (required + test + extra).

#### *property* env_vars_defaults *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [str](https://docs.python.org/3/builtins/stdtypes.html#str)]*

Get default environment variables.

#### *property* env_vars_extra *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]*

Get extra/optional environment variable names (no warning if missing).

#### *property* env_vars_required *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]*

Get required environment variable names (CI fails if not in secrets).

#### *property* env_vars_test *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]*

Get test environment variable names (CI warns if not in secrets).

#### *property* exclude_paths *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]*

Get test paths to exclude.

#### *classmethod* from_file(pyproject_path)

Load CI configuration from a pyproject.toml file.

* **Parameters:**
  **pyproject_path** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)) – Path to pyproject.toml file or directory containing it
* **Return type:**
  [`CIConfig`](_autosummary/wads.ci_config.html.md#wads.ci_config.CIConfig)
* **Returns:**
  CIConfig instance

#### generate_env_block()

Generate the workflow-level YAML env block for GitHub Actions.

Contains ONLY non-secret values:

- PROJECT_NAME (from config)
- Literal defaults from [tool.wads.ci.env.defaults]

Secret-backed vars are deliberately NOT emitted at workflow level: a
workflow-level secret is in scope for the setup job too, and GitHub
refuses to emit any job OUTPUT containing a secret’s value — a short
value (e.g. a test level of “3”) silently blanks `python-versions`
and empties the test matrix (issue #61). Secret-backed vars are
scoped to the jobs that run tests via generate_test_env_block /
generate_test_env_vars.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)
* **Returns:**
  YAML string for the workflow-level env section

#### generate_env_vars_yaml()

Generate YAML lines for all environment variables to be set from secrets.
This is used in the #ENV_VARS# placeholder in CI templates.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)
* **Returns:**
  YAML string with conditional env var assignments

#### generate_github_pages_job()

Generate YAML for GitHub Pages job.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)
* **Returns:**
  YAML string for GitHub Pages job, or empty string if disabled

#### generate_pre_test_steps(platform='linux')

Generate YAML steps for pre-test commands and system dependencies.

Supports legacy [tool.wads.ci.testing.system_dependencies] format.

* **Parameters:**
  **platform** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Platform identifier (‘linux’, ‘macos’, ‘windows’)
* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)
* **Returns:**
  YAML string for pre-test steps, or empty string if no commands/deps

#### generate_stub_secrets_block()

Render the caller-stub `secrets:` pass-through for this repo.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

#### generate_test_env_block()

Generate a whole job-level `env:` block for the validation job.

Returns ‘’ when no secret-backed vars are left to emit, so the
rendered job simply has no `env:` key (an empty `env:` mapping is
invalid).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

#### generate_test_env_vars(, indent=6, exclude=())

Generate job-level env entry lines for secret-backed vars.

One `VAR: ${{ secrets.NAME || '' }}` line per name declared in
required_envvars / test_envvars / extra_envvars, honoring
[tool.wads.ci.env.secret_aliases] (env-var name -> backing secret
name). An unset secret renders as an empty string. Returns ‘’ when
nothing is left to emit.

Names also present in [tool.wads.ci.env].defaults are skipped: the
committed default is authoritative (matching the reusable workflow’s
export-ci-env), and it already reaches every job from workflow level
— re-emitting `${{ secrets.X || '' }}` at job level would override
the default with ‘’ in exactly the jobs that run tests.

Publish credentials (`UV_PUBLISH_TOKEN -> PYPI_PASSWORD` and the
like; see `wads.scripts.export_ci_env.is_publish_credential`) are
never rendered into a test job, with a note on stderr (issue #110).

* **Parameters:**
  * **indent** ([`int`](https://docs.python.org/3/builtins/functions.html#int)) – leading spaces per line (6 = entries of a job-level
    `env:` block)
  * **exclude** ([`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)) – additional names to skip (e.g. keys the target job
    already sets literally)
* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

#### generate_windows_validation_job()

Generate YAML for Windows validation job.

Supports legacy system_dependencies format.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)
* **Returns:**
  YAML string for Windows job, or empty string if disabled

#### has_ci_config()

Check if any CI configuration is present.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

#### *property* install_extras *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

Extras to install in CI, as a comma-separated string for pip’s
`.[extras]` syntax.

Read from `[tool.wads.ci.install].extras` (accepts a list or a
comma-separated string). Returns `""` when unset, in which case CI
installs only the package’s core dependencies. Example: a project whose
tests need its heavier extra can set `extras = "create"` so the CI job
installs `.[create]`.

#### *property* installer *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

Get the package installer to use in CI (‘uv’ or ‘pip’).

#### is_black_enabled()

Check if Black formatter is enabled.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

#### is_mypy_enabled()

Check if Mypy type checker is enabled.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

#### is_ruff_enabled()

Check if Ruff linter is enabled.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

#### *property* licence_config *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)*

The `[tool.wads.licence]` table.

Note it sits under `[tool.wads]`, NOT under `[tool.wads.ci]`: the
policy is a fact about the package’s dependency perimeter, and
[`wads.licence_check`](_autosummary/wads.licence_check.html.md#module-wads.licence_check) is useful outside CI. Only the `enabled`
flag below is a CI concern.

#### *property* licence_enabled *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

Whether CI should run the licence-perimeter gate.

Defaults to **False**. This one is opt-in rather than opt-out, unlike
every other gate here, and deliberately so: the reusable workflow is
called by the whole fleet, and turning a new failing check on for all
of them at once reddens every caller’s CI in one merge. Set
`[tool.wads.licence].enabled = true` per repo, once its exposures are
resolved.

#### *property* metrics_config *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)*

Get code metrics configuration.

#### *property* metrics_config_path *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

Get path to umpyre config file.

#### *property* metrics_enabled *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

Check if code metrics tracking is enabled.

#### *property* metrics_force_run *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

Check if metrics should run even on workflow failure.

#### *property* metrics_python_version *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

Get Python version for metrics collection.

#### *property* metrics_storage_branch *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

Get git branch to store metrics data.

#### *property* ops *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)*

Get system dependencies configuration from [tool.wads.ops.\*] sections.

* **Returns:**
  Dictionary mapping dependency names to their configuration

#### *property* project_name *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

Get the project name for CI.

#### *property* publish_config *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)*

Get publish configuration.

#### *property* publish_enabled *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

Check if publishing is enabled.

When True (the default), the publish job runs on main/master unless the
commit *subject line* contains [`publish_skip_ci_marker`](_autosummary/wads.ci_config.html.md#wads.ci_config.CIConfig.publish_skip_ci_marker). When
False, the publish job is skipped unless the commit *subject line*
contains [`publish_marker`](_autosummary/wads.ci_config.html.md#wads.ci_config.CIConfig.publish_marker).

Both markers are matched against the first line of the commit message
only. A squash-merge folds the entire PR body into the squash commit
message, so a full-message match fires on a PR that merely writes
about a marker.

#### *property* publish_marker *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

Commit-subject substring that forces publishing.

Used when publishing is disabled: a commit whose *subject line* (the
first line of its message) contains this marker still runs the publish
job. Defaults to `"[publish]"`.

#### *property* publish_skip_ci_marker *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

Commit-subject substring that suppresses publishing.

Used when publishing is enabled: a commit whose *subject line* (the
first line of its message) contains this marker skips the publish job.
Defaults to `"[skip ci]"` (which GitHub also natively recognises to
skip the whole workflow).

#### *property* pytest_args *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]*

Get pytest arguments.

#### *property* python_versions *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]*

Get Python versions to test against.

#### *property* quality_config *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)*

Get code quality tool configuration.

#### *property* run_ci_marker *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

Commit-subject substring that runs CI in `"on-demand"` mode.

Defaults to `"[run ci]"`. Matched against the first line of the
commit message only (a squash-merge folds the PR body into the rest).
Must be a non-empty single line: an empty marker would match every
subject and silently turn on-demand back into auto.

#### stub_secret_names(, always=('PYPI_PASSWORD',))

GitHub *secret* names the caller stub should pass to the reusable workflow.

This is the per-repo transport list (kept small): `PYPI_PASSWORD` (for
the publish job) plus the backing secret of every env var the repo
declares in `[tool.wads.ci.env]` (required/test/extra), resolved
through `secret_aliases`. Order-preserving, de-duplicated.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

#### *property* system_dependencies *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict) | [list](https://docs.python.org/3/builtins/stdtypes.html#list)*

Get system dependencies for CI environments (DEPRECATED).

DEPRECATED: Use [tool.wads.ops.\*] format with install-system-deps action instead.
This property is maintained for backward compatibility with legacy format.

Returns either:

- A list of package names (Ubuntu only)
- A dict with platform keys: ubuntu, macos, windows

#### *property* test_on_windows *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

Check if Windows testing is enabled.

#### *property* testing_config *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)*

Get testing configuration.

#### *property* tests_enabled *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

Check if the CI test step should run.

Defaults to True. Set `[tool.wads.ci.testing].enabled = false` to skip
the pytest/doctest step entirely — useful for apps or repos that have
no real test suite and whose modules are not import-clean.

#### to_ci_env_block()

Generate YAML env block for GitHub Actions.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)
* **Returns:**
  YAML string for env section

#### to_ci_template_substitutions()

Generate all template substitutions for CI workflow generation.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]
* **Returns:**
  Dictionary mapping placeholder names to their values

#### to_pre_test_step()

Generate YAML step for pre-test commands.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]
* **Returns:**
  YAML string for pre-test step, or None if no commands

#### *property* trigger_config *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)*

Get the `[tool.wads.ci.trigger]` table.

#### *property* trigger_mode *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

`"auto"` (the default) or `"on-demand"`.

`"auto"` runs on every push and pull request, as wads always has.
`"on-demand"` means **nothing runs unless asked**: every job (tests,
publish, Pages) runs only when the commit *subject line* contains
[`run_ci_marker`](_autosummary/wads.ci_config.html.md#wads.ci_config.CIConfig.run_ci_marker), or on a manual `workflow_dispatch`.

Raises `ValueError` on any other value, so a typo fails the CI setup
job loudly rather than silently choosing a mode.

```pycon
>>> CIConfig({}).trigger_mode
'auto'
>>> CIConfig({"tool": {"wads": {"ci": {"trigger": {"mode": "on-demand"}}}}}).trigger_mode
'on-demand'
```

* **Type:**
  When CI runs

#### *property* uninstalled_test_extras *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]]*

Test-looking extras CI will not install, mapped to what they would add.

Non-empty only when `[tool.wads.ci.install].extras` is absent: CI then
installs core dependencies only, so an extra named like
[`TEST_EXTRA_NAMES`](_autosummary/wads.ci_config.html.md#wads.ci_config.CIConfig.TEST_EXTRA_NAMES) whose packages go beyond
[`CI_PROVIDED_PACKAGES`](_autosummary/wads.ci_config.html.md#wads.ci_config.CIConfig.CI_PROVIDED_PACKAGES) is silently missing from the test job
(i2mint/wads#59). Any explicit `extras` value, including `""`, is
a decision and silences this.

```pycon
>>> config = CIConfig({'project': {'name': 'p', 'optional-dependencies': {
...     'dev': ['pytest', 'httpx>=0.27'], 'docs': ['sphinx']}}})
>>> config.uninstalled_test_extras
{'dev': ['httpx']}
```

#### *property* windows_blocking *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

Whether a failing Windows leg should turn the CI run red.

Defaults to False — the historical behaviour, where the Windows job is
informational (`continue-on-error`) and a Windows-only defect merges
behind a green tick. Opt in with `windows_blocking = true` under
`[tool.wads.ci.testing]`.

Takes effect in the uv workflows (the reusable
`.github/workflows/uv-ci.yml` and its `wads/data/github_ci_uv.yml`
mirror), via the `windows-blocking` output of the read-ci-config
action. It makes the RUN red; it does not gate publication —
`publish` does not depend on the Windows job.

### wads.ci_config.get_ci_config_or_defaults(pyproject_path, project_name=None)

Read CI configuration from pyproject.toml, using defaults if file doesn’t exist.

* **Parameters:**
  * **pyproject_path** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)) – Path to pyproject.toml file or directory containing it
  * **project_name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Default project name if not found in config
* **Return type:**
  [`CIConfig`](_autosummary/wads.ci_config.html.md#wads.ci_config.CIConfig)
* **Returns:**
  CIConfig instance with defaults if file doesn’t exist

### wads.ci_config.read_ci_config(pyproject_path)

Read CI configuration from pyproject.toml.

* **Parameters:**
  **pyproject_path** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)) – Path to pyproject.toml file or directory containing it
* **Return type:**
  [`CIConfig`](_autosummary/wads.ci_config.html.md#wads.ci_config.CIConfig)
* **Returns:**
  CIConfig instance with project configuration

Usage:

```default
config = read_ci_config("path/to/project")
print(config.project_name)        # Project name from pyproject.toml
print(config.python_versions)     # Python versions to test
```

### wads.ci_config.render_minimal_env_placeholders(template_text, project_name)

Render the inline template’s env placeholders without a [tool.wads.ci]
config: workflow-level env gets PROJECT_NAME only, and the test-job
placeholders render empty (there are no declared secret-backed vars).

Used by the no-pyproject fallbacks in migration and populate so a shipped
workflow never carries literal placeholder lines.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)


# _autosummary/wads.ci_local.html.md

# wads.ci_local

Do locally what the wads CI would have done: the same `[tool.wads.ci]`, no Actions minutes.

`wads ci-local` reads the repo’s `pyproject.toml` through [`wads.ci_config.CIConfig`](_autosummary/wads.ci_config.html.md#wads.ci_config.CIConfig),
the object the reusable workflow’s setup job reads, and runs in order:

1. **lint**: `ruff format --check` (reported, never fatal: CI reformats in place and moves
   on), then `ruff check <project>`, both through `uvx` so the repo’s own `[tool.ruff]`
   applies exactly as in CI. Black and mypy too, when enabled.
2. **tests**: for each `python_versions` entry, a fresh `uv` venv in a temporary
   directory, `uv pip install -e .[extras]`, then pytest with the arguments
   `actions/run-tests-uv` builds (coverage, `--doctest-modules` and its option flags,
   one `--ignore` per excluded path, then `pytest_args`).
3. **build**: `uv build` into the temporary directory, never the repo’s `dist/`, so a
   stale artifact can never be uploaded.

With `publish=True` it does what the publish job does in place of the plain build:
format in place, bump the version with `isee` (as CI does), build, `uv publish`,
commit, tag and push. Before any of that it refuses on a dirty tree, a branch other than
the default, a branch behind its remote, or missing PyPI credentials.

Known differences from CI: the developer’s environment passes through to the commands
(minus PyPI credentials, which only the upload step sees), where CI exports only the
variables `[tool.wads.ci.env]` declares; `[tool.wads.ops.*]` system packages are not
installed; and lint runs on the tree as it is, where CI formats first.

Every command goes through one `runner` seam. The default runs subprocesses; tests pass
a fake. The default also refuses upload and push steps while pytest is running, so no test
can ever spend or publish.

```pycon
>>> steps = plan_ci_local(".", config=CIConfig({"project": {"name": "pkg"}}))
>>> [s.name for s in steps][:3]
['format check (ruff)', 'lint (ruff)', 'python 3.10: venv']
```

### Functions

| [`ci_local`](_autosummary/wads.ci_local.html.md#wads.ci_local.ci_local)([repo, publish, dry_run])               | Do locally what wads CI would have done, from the repo's [tool.wads.ci] config.                                               |
|---------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------|
| [`plan_ci_local`](_autosummary/wads.ci_local.html.md#wads.ci_local.plan_ci_local)([repo, publish, config, platform]) | Everything `wads ci-local` would run for `repo`, in order, without running it.                                                |
| [`pytest_argv`](_autosummary/wads.ci_local.html.md#wads.ci_local.pytest_argv)(config)                              | The pytest arguments `actions/run-tests-uv` builds, in its order.                                                             |
| [`render_plan`](_autosummary/wads.ci_local.html.md#wads.ci_local.render_plan)(steps)                               | The plan as numbered lines, flagging non-blocking and side-effecting steps.                                                   |
| [`resolve_pypi_token`](_autosummary/wads.ci_local.html.md#wads.ci_local.resolve_pypi_token)(\*[, environ, pypirc])        | Find a PyPI API token, most specific source first.                                                                            |
| [`run_ci_local`](_autosummary/wads.ci_local.html.md#wads.ci_local.run_ci_local)([repo, publish, config, ...])       | Run [`plan_ci_local()`](_autosummary/wads.ci_local.html.md#wads.ci_local.plan_ci_local)'s steps, stop at the first blocking failure, and report. |
| [`subprocess_runner`](_autosummary/wads.ci_local.html.md#wads.ci_local.subprocess_runner)(argv, \*, cwd, env[, ...])     | The default runner: really run `argv`, streaming its output.                                                                  |
| [`uv_build_args`](_autosummary/wads.ci_local.html.md#wads.ci_local.uv_build_args)(\*[, sdist, wheel])                | `uv build` selection flags for `[tool.wads.ci.build]`.                                                                        |
| [`venv_python`](_autosummary/wads.ci_local.html.md#wads.ci_local.venv_python)(venv, \*[, platform])                | Path of the interpreter inside a venv directory.                                                                              |

### Classes

| [`CILocalReport`](_autosummary/wads.ci_local.html.md#wads.ci_local.CILocalReport)(project, publish, ...[, ...])   | The outcome of a `wads ci-local` run, rendered the way CI would summarise it.   |
|------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------|
| [`RunResult`](_autosummary/wads.ci_local.html.md#wads.ci_local.RunResult)(returncode[, stdout])               | What a runner reports back: the exit code, and stdout when it was captured.     |
| [`Step`](_autosummary/wads.ci_local.html.md#wads.ci_local.Step)(name[, argv, check, description, ...])   | One thing CI would do: a command (`argv`) or an in-process `check`.             |

### *class* wads.ci_local.CILocalReport(project, publish, trigger_mode, python_versions, outcomes=<factory>, notes=<factory>, values=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

The outcome of a `wads ci-local` run, rendered the way CI would summarise it.

#### *property* failure *: [Step](_autosummary/wads.ci_local.html.md#wads.ci_local.Step) | [None](https://docs.python.org/3/builtins/constants.html#None)*

The blocking step that failed, if any.

### *class* wads.ci_local.RunResult(returncode, stdout='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

What a runner reports back: the exit code, and stdout when it was captured.

### *class* wads.ci_local.Step(name, argv=(), check=None, description='', blocking=True, side_effect='', capture='', env=())

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One thing CI would do: a command (`argv`) or an in-process `check`.

`argv` and `env` values may hold `<<workdir>>`, `<<version>>`,
`<<default_branch>>` or `<<pypi_token>>`, filled in at run time. A non-`blocking`
failure is reported without stopping the run. `capture` names the context value that
receives the command’s last stdout line. A `check` takes the run context and returns
an error message, or `None` when it passes.

### wads.ci_local.ci_local(repo='.', , publish=False, dry_run=False)

Do locally what wads CI would have done, from the repo’s [tool.wads.ci] config.

Lint, tests (a fresh uv venv per configured Python) and build. With –publish, the
publish job instead: format, bump version, build, upload to PyPI, commit, tag and push,
refusing on a dirty tree, off the default branch, or without PyPI credentials. With
–dry-run, print the plan and run nothing.

### wads.ci_local.plan_ci_local(repo='.', , publish=False, config=None, platform='posix')

Everything `wads ci-local` would run for `repo`, in order, without running it.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Step`](_autosummary/wads.ci_local.html.md#wads.ci_local.Step)]

### wads.ci_local.pytest_argv(config)

The pytest arguments `actions/run-tests-uv` builds, in its order.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> pytest_argv(CIConfig({"project": {"name": "pkg"}}))
['--cov=pkg', '--cov-report=term-missing', '--doctest-modules',
 '-o', 'doctest_optionflags=ELLIPSIS IGNORE_EXCEPTION_DETAIL',
 '--ignore=examples', '--ignore=scrap', '-v', '--tb=short']
```

### wads.ci_local.render_plan(steps)

The plan as numbered lines, flagging non-blocking and side-effecting steps.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### wads.ci_local.resolve_pypi_token(, environ=None, pypirc=None)

Find a PyPI API token, most specific source first. Returns `(token, source)`.

1. `$PYPI_PASSWORD`, the secret name CI reads;
2. `$UV_PUBLISH_TOKEN`, what `uv publish` itself reads;
3. `~/.pypirc`, section `[pypi]`: `password`, when `username` is `__token__`
   (or absent) or the password is a `pypi-` token.

Token-only, like CI. The token itself is never printed.

```pycon
>>> resolve_pypi_token(environ={"PYPI_PASSWORD": "pypi-x", "UV_PUBLISH_TOKEN": "pypi-y"})
('pypi-x', '$PYPI_PASSWORD')
>>> resolve_pypi_token(environ={}, pypirc="/nonexistent/.pypirc")
(None, '')
```

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### wads.ci_local.run_ci_local(repo='.', \*, publish=False, config=None, runner=<function subprocess_runner>, environ=None, out=None, keep_workdir=False)

Run [`plan_ci_local()`](_autosummary/wads.ci_local.html.md#wads.ci_local.plan_ci_local)’s steps, stop at the first blocking failure, and report.

* **Return type:**
  [`CILocalReport`](_autosummary/wads.ci_local.html.md#wads.ci_local.CILocalReport)

### wads.ci_local.subprocess_runner(argv, , cwd, env, capture=False, side_effect='')

The default runner: really run `argv`, streaming its output.

Refuses outward side effects (upload, push) while pytest is running.

* **Return type:**
  [`RunResult`](_autosummary/wads.ci_local.html.md#wads.ci_local.RunResult)

### wads.ci_local.uv_build_args(, sdist=True, wheel=True)

`uv build` selection flags for `[tool.wads.ci.build]`.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> uv_build_args()
[]
>>> uv_build_args(sdist=False)
['--wheel']
>>> uv_build_args(wheel=False)
['--sdist']
```

### wads.ci_local.venv_python(venv, , platform='posix')

Path of the interpreter inside a venv directory.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> venv_python("v", platform="posix")
'v/bin/python'
>>> venv_python("v", platform="nt")
'v/Scripts/python.exe'
```


# _autosummary/wads.ci_secrets.html.md

# wads.ci_secrets

Canonical registry of CI secret names for wads-managed projects.

This module is the single source of truth for the *transport* layer of wads
CI secrets: what the reusable workflow (`uv-ci.yml`) declares in
`on.workflow_call.secrets` and what the caller stub passes.

## Transport: named by default, one JSON secret on request

A GitHub *reusable* workflow’s secret interface (`on.workflow_call.secrets`)
must be **static YAML** — it is parsed before any job runs and cannot be
parametrized from `pyproject.toml`. `secrets: inherit` is documented to
work only when caller and callee share an org/enterprise, so it is unusable
for personal-account repos calling an `i2mint`-owned workflow.

**Named (the default).** The workflow declares a *superset* of optional
secret names ([`DEFAULT_CI_SECRETS`](_autosummary/wads.ci_secrets.html.md#wads.ci_secrets.DEFAULT_CI_SECRETS)), and each repo’s stub passes, by
name, only `PYPI_PASSWORD` plus the backing secret of each env var its
`[tool.wads.ci.env]` declares:

```default
secrets:
  PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
  OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
```

The called workflow receives those secrets and nothing else, each masked by
GitHub on its own. The bundled stub template is named; `populate` and every
re-render (`wads-migrate ci-to-stub`, `ci-on-demand`, `fleet-stub`)
write a named stub, converting a JSON one, unless `--transport json` is
asked for.

A name outside the superset makes GitHub reject the workflow at parse time
with an opaque `startup_failure` (issue #63). The superset therefore grows
**additively**: adding a `required: false` name never breaks an existing
stub (removing one can). `wads-secrets add` and `wads-migrate` warn about
out-of-superset names, and a re-render with no explicit transport falls back
to JSON for such a repo rather than write a stub that cannot start.

**JSON (opt-in).** `wads-migrate ci-to-stub --transport json` passes one
statically-declared secret, `JSON_TRANSPORT_SECRET`
(`WADS_CI_SECRETS_JSON`), whose value is the caller’s whole `secrets`
context:

```default
secrets:
  WADS_CI_SECRETS_JSON: ${{ toJSON(toJSON(secrets)) }}
```

Any secret name then reaches the workflow — but so does every secret the repo
can read, org-level ones included, and serialising the whole `secrets`
context into a workflow in another repository is structurally what a
secret-exfiltration workflow looks like. GitHub’s malicious-workflow scanner
holds such runs (`action_required`, zero jobs, no log, and the REST approve
endpoint refuses them; issues #74, #88, #101). It was seen first on new
repositories and later on an existing one; switching to named made the next
push run every time. Two details of the JSON form, verified empirically
(issue #63):

* **Double encoding** (`toJSON(toJSON(...))`) makes the transported value a
  *single-line* JSON string. A multiline secret is masked per line, and the
  pretty-printed form’s `{` / `}` lines would become global masks that
  mangle every brace in the job log.
* **Re-masking**: values extracted from the blob are *not* automatically
  masked in the callee, so the `export-ci-env` action emits
  `::add-mask::` for each secret-sourced value before exporting it.

So there are two layers:

* **Transport** — named (default) or the JSON secret (opt-in), rendered into
  static YAML. Plumbing.
* **Env-assignment** — pyproject-driven, exact, per-repo: *which* transported
  secrets land in the job env, and which are required (see
  [`wads.ci_config`](_autosummary/wads.ci_config.html.md#module-wads.ci_config) and the `export-ci-env` action). Nothing is exported
  > unless declared there. The thing users tune.

Keeping the names here (Python) and *rendering* them into the YAML (with a
test pinning the YAML to this module) gives a single SSOT while respecting
GitHub’s parse-time-literal constraint.

### Module Attributes

| [`DEFAULT_CI_SECRETS`](_autosummary/wads.ci_secrets.html.md#wads.ci_secrets.DEFAULT_CI_SECRETS)    | Ordered, de-duplicated superset of names a named-transport stub may pass (additive).   |
|------------------------------------------------------------------------|----------------------------------------------------------------------------------------|
| [`WORKFLOW_CALL_SECRETS`](_autosummary/wads.ci_secrets.html.md#wads.ci_secrets.WORKFLOW_CALL_SECRETS) | the opt-in JSON transport secret + the superset.                                       |

### Functions

| [`is_valid_secret_name`](_autosummary/wads.ci_secrets.html.md#wads.ci_secrets.is_valid_secret_name)(name)                        | Return `True` iff `name` is already a safe, valid secret name.              |
|----------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------|
| [`normalize_secret_name`](_autosummary/wads.ci_secrets.html.md#wads.ci_secrets.normalize_secret_name)(name)                       | Force `name` to a valid `UPPER_SNAKE` secret/env-var name.                  |
| [`render_stub_json_transport`](_autosummary/wads.ci_secrets.html.md#wads.ci_secrets.render_stub_json_transport)(\*[, indent])          | Render the caller stub's JSON-transport `secrets:` line (opt-in).           |
| [`render_stub_secrets_passthrough`](_autosummary/wads.ci_secrets.html.md#wads.ci_secrets.render_stub_secrets_passthrough)([names, indent])  | Render a caller stub's *named* `secrets:` pass-through block (the default). |
| [`render_workflow_call_secrets`](_autosummary/wads.ci_secrets.html.md#wads.ci_secrets.render_workflow_call_secrets)([names, indent])     | Render the `on.workflow_call.secrets:` body for the reusable workflow.      |
| [`stub_transport`](_autosummary/wads.ci_secrets.html.md#wads.ci_secrets.stub_transport)(stub)                              | The secrets transport of a stub's content: `"json"` or `"named"`.           |
| [`stub_with_named_transport`](_autosummary/wads.ci_secrets.html.md#wads.ci_secrets.stub_with_named_transport)(stub, names)            | Rewrite a stub's secrets region to the named transport, passing `names`.    |
| [`stub_with_transport`](_autosummary/wads.ci_secrets.html.md#wads.ci_secrets.stub_with_transport)(stub, \*[, transport, names]) | Rewrite a stub's secrets region to the given transport.                     |
| [`warn_names_outside_superset`](_autosummary/wads.ci_secrets.html.md#wads.ci_secrets.warn_names_outside_superset)(names)                | Warn loudly for names a named-transport stub cannot legally pass.           |

### Exceptions

| [`InvalidSecretName`](_autosummary/wads.ci_secrets.html.md#wads.ci_secrets.InvalidSecretName)   | Raised when a string cannot be a safe, valid secret/env-var name.   |
|----------------------------------------------------------------------|---------------------------------------------------------------------|

### wads.ci_secrets.DEFAULT_CI_SECRETS *= ('PYPI_PASSWORD', 'TEST_PYPI_PASSWORD', 'NPM_TOKEN', 'SSH_PRIVATE_KEY', 'CODECOV_TOKEN', 'DOCKERHUB_USERNAME', 'DOCKERHUB_TOKEN', 'OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'GEMINI_API_KEY', 'GOOGLE_API_KEY', 'DEEPSEEK_API_KEY', 'OPENROUTER_API_KEY', 'GROQ_API_KEY', 'MISTRAL_API_KEY', 'COHERE_API_KEY', 'PERPLEXITY_API_KEY', 'REPLICATE_API_TOKEN', 'TOGETHER_API_KEY', 'XAI_API_KEY', 'FAL_KEY', 'AZURE_OPENAI_API_KEY', 'AZURE_OPENAI_ENDPOINT', 'TAVILY_API_KEY', 'COMPOSIO_API_KEY', 'SKILLSDIRECTORY_API_KEY', 'NEWSDATA_API_KEY', 'LANGSMITH_API_KEY', 'HF_TOKEN', 'HF_WRITE_TOKEN', 'HUGGINGFACE_TOKEN', 'KAGGLE_USERNAME', 'KAGGLE_KEY', 'WANDB_API_KEY', 'PINECONE_API_KEY', 'ELEVEN_API_KEY', 'ELEVENLABS_API_KEY', 'SUNO_API_KEY', 'SPOTIFY_API_CLIENT_ID', 'SPOTIFY_API_CLIENT_SECRET', 'SPOTIPY_CLIENT_ID', 'SPOTIPY_CLIENT_SECRET', 'ALPACA_API_KEY', 'ALPACA_SECRET_KEY', 'APCA_API_KEY_ID', 'APCA_API_SECRET_KEY', 'AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_SESSION_TOKEN', 'GCP_SA_KEY', 'GOOGLE_APPLICATION_CREDENTIALS_JSON', 'SLACK_BOT_TOKEN', 'SLACK_WEBHOOK_URL', 'DISCORD_WEBHOOK_URL', 'TELEGRAM_BOT_TOKEN', 'TWILIO_ACCOUNT_SID', 'TWILIO_AUTH_TOKEN', 'SENDGRID_API_KEY', 'STRIPE_SECRET_KEY', 'DATABASE_URL', 'MONGODB_URI', 'REDIS_URL')*

Ordered, de-duplicated superset of names a named-transport stub may pass (additive).

### *exception* wads.ci_secrets.InvalidSecretName

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

Raised when a string cannot be a safe, valid secret/env-var name.

### wads.ci_secrets.WORKFLOW_CALL_SECRETS *= ('WADS_CI_SECRETS_JSON', 'PYPI_PASSWORD', 'TEST_PYPI_PASSWORD', 'NPM_TOKEN', 'SSH_PRIVATE_KEY', 'CODECOV_TOKEN', 'DOCKERHUB_USERNAME', 'DOCKERHUB_TOKEN', 'OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'GEMINI_API_KEY', 'GOOGLE_API_KEY', 'DEEPSEEK_API_KEY', 'OPENROUTER_API_KEY', 'GROQ_API_KEY', 'MISTRAL_API_KEY', 'COHERE_API_KEY', 'PERPLEXITY_API_KEY', 'REPLICATE_API_TOKEN', 'TOGETHER_API_KEY', 'XAI_API_KEY', 'FAL_KEY', 'AZURE_OPENAI_API_KEY', 'AZURE_OPENAI_ENDPOINT', 'TAVILY_API_KEY', 'COMPOSIO_API_KEY', 'SKILLSDIRECTORY_API_KEY', 'NEWSDATA_API_KEY', 'LANGSMITH_API_KEY', 'HF_TOKEN', 'HF_WRITE_TOKEN', 'HUGGINGFACE_TOKEN', 'KAGGLE_USERNAME', 'KAGGLE_KEY', 'WANDB_API_KEY', 'PINECONE_API_KEY', 'ELEVEN_API_KEY', 'ELEVENLABS_API_KEY', 'SUNO_API_KEY', 'SPOTIFY_API_CLIENT_ID', 'SPOTIFY_API_CLIENT_SECRET', 'SPOTIPY_CLIENT_ID', 'SPOTIPY_CLIENT_SECRET', 'ALPACA_API_KEY', 'ALPACA_SECRET_KEY', 'APCA_API_KEY_ID', 'APCA_API_SECRET_KEY', 'AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_SESSION_TOKEN', 'GCP_SA_KEY', 'GOOGLE_APPLICATION_CREDENTIALS_JSON', 'SLACK_BOT_TOKEN', 'SLACK_WEBHOOK_URL', 'DISCORD_WEBHOOK_URL', 'TELEGRAM_BOT_TOKEN', 'TWILIO_ACCOUNT_SID', 'TWILIO_AUTH_TOKEN', 'SENDGRID_API_KEY', 'STRIPE_SECRET_KEY', 'DATABASE_URL', 'MONGODB_URI', 'REDIS_URL')*

the opt-in JSON transport secret + the superset.

* **Type:**
  Every secret `uv-ci.yml` declares

### wads.ci_secrets.is_valid_secret_name(name)

Return `True` iff `name` is already a safe, valid secret name.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

```pycon
>>> is_valid_secret_name("OPENAI_API_KEY")
True
>>> is_valid_secret_name("my-key")
False
>>> is_valid_secret_name("GITHUB_TOKEN")
False
```

### wads.ci_secrets.normalize_secret_name(name)

Force `name` to a valid `UPPER_SNAKE` secret/env-var name.

Hyphens, dots and spaces become underscores; the result is upper-cased.
Inputs that look like a *value* rather than a *name* are rejected, to guard
against accidentally committing a secret value.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> normalize_secret_name("my-api.key")
'MY_API_KEY'
>>> normalize_secret_name("OPENAI_API_KEY")
'OPENAI_API_KEY'
>>> normalize_secret_name("  hf token  ")
'HF_TOKEN'
```

Rejects value-looking and reserved inputs:

```pycon
>>> normalize_secret_name("sk-proj-aBc/123+xyz=")
Traceback (most recent call last):
InvalidSecretName: ...
>>> normalize_secret_name("GITHUB_TOKEN")
Traceback (most recent call last):
InvalidSecretName: ...
```

### wads.ci_secrets.render_stub_json_transport(, indent=6)

Render the caller stub’s JSON-transport `secrets:` line (opt-in).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> print(render_stub_json_transport())
      WADS_CI_SECRETS_JSON: ${{ toJSON(toJSON(secrets)) }}
```

### wads.ci_secrets.render_stub_secrets_passthrough(names=('PYPI_PASSWORD', 'TEST_PYPI_PASSWORD', 'NPM_TOKEN', 'SSH_PRIVATE_KEY', 'CODECOV_TOKEN', 'DOCKERHUB_USERNAME', 'DOCKERHUB_TOKEN', 'OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'GEMINI_API_KEY', 'GOOGLE_API_KEY', 'DEEPSEEK_API_KEY', 'OPENROUTER_API_KEY', 'GROQ_API_KEY', 'MISTRAL_API_KEY', 'COHERE_API_KEY', 'PERPLEXITY_API_KEY', 'REPLICATE_API_TOKEN', 'TOGETHER_API_KEY', 'XAI_API_KEY', 'FAL_KEY', 'AZURE_OPENAI_API_KEY', 'AZURE_OPENAI_ENDPOINT', 'TAVILY_API_KEY', 'COMPOSIO_API_KEY', 'SKILLSDIRECTORY_API_KEY', 'NEWSDATA_API_KEY', 'LANGSMITH_API_KEY', 'HF_TOKEN', 'HF_WRITE_TOKEN', 'HUGGINGFACE_TOKEN', 'KAGGLE_USERNAME', 'KAGGLE_KEY', 'WANDB_API_KEY', 'PINECONE_API_KEY', 'ELEVEN_API_KEY', 'ELEVENLABS_API_KEY', 'SUNO_API_KEY', 'SPOTIFY_API_CLIENT_ID', 'SPOTIFY_API_CLIENT_SECRET', 'SPOTIPY_CLIENT_ID', 'SPOTIPY_CLIENT_SECRET', 'ALPACA_API_KEY', 'ALPACA_SECRET_KEY', 'APCA_API_KEY_ID', 'APCA_API_SECRET_KEY', 'AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_SESSION_TOKEN', 'GCP_SA_KEY', 'GOOGLE_APPLICATION_CREDENTIALS_JSON', 'SLACK_BOT_TOKEN', 'SLACK_WEBHOOK_URL', 'DISCORD_WEBHOOK_URL', 'TELEGRAM_BOT_TOKEN', 'TWILIO_ACCOUNT_SID', 'TWILIO_AUTH_TOKEN', 'SENDGRID_API_KEY', 'STRIPE_SECRET_KEY', 'DATABASE_URL', 'MONGODB_URI', 'REDIS_URL'), , indent=6)

Render a caller stub’s *named* `secrets:` pass-through block (the default).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> print(render_stub_secrets_passthrough(["PYPI_PASSWORD", "NPM_TOKEN"]))
      PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
      NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
```

### wads.ci_secrets.render_workflow_call_secrets(names=('WADS_CI_SECRETS_JSON', 'PYPI_PASSWORD', 'TEST_PYPI_PASSWORD', 'NPM_TOKEN', 'SSH_PRIVATE_KEY', 'CODECOV_TOKEN', 'DOCKERHUB_USERNAME', 'DOCKERHUB_TOKEN', 'OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'GEMINI_API_KEY', 'GOOGLE_API_KEY', 'DEEPSEEK_API_KEY', 'OPENROUTER_API_KEY', 'GROQ_API_KEY', 'MISTRAL_API_KEY', 'COHERE_API_KEY', 'PERPLEXITY_API_KEY', 'REPLICATE_API_TOKEN', 'TOGETHER_API_KEY', 'XAI_API_KEY', 'FAL_KEY', 'AZURE_OPENAI_API_KEY', 'AZURE_OPENAI_ENDPOINT', 'TAVILY_API_KEY', 'COMPOSIO_API_KEY', 'SKILLSDIRECTORY_API_KEY', 'NEWSDATA_API_KEY', 'LANGSMITH_API_KEY', 'HF_TOKEN', 'HF_WRITE_TOKEN', 'HUGGINGFACE_TOKEN', 'KAGGLE_USERNAME', 'KAGGLE_KEY', 'WANDB_API_KEY', 'PINECONE_API_KEY', 'ELEVEN_API_KEY', 'ELEVENLABS_API_KEY', 'SUNO_API_KEY', 'SPOTIFY_API_CLIENT_ID', 'SPOTIFY_API_CLIENT_SECRET', 'SPOTIPY_CLIENT_ID', 'SPOTIPY_CLIENT_SECRET', 'ALPACA_API_KEY', 'ALPACA_SECRET_KEY', 'APCA_API_KEY_ID', 'APCA_API_SECRET_KEY', 'AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_SESSION_TOKEN', 'GCP_SA_KEY', 'GOOGLE_APPLICATION_CREDENTIALS_JSON', 'SLACK_BOT_TOKEN', 'SLACK_WEBHOOK_URL', 'DISCORD_WEBHOOK_URL', 'TELEGRAM_BOT_TOKEN', 'TWILIO_ACCOUNT_SID', 'TWILIO_AUTH_TOKEN', 'SENDGRID_API_KEY', 'STRIPE_SECRET_KEY', 'DATABASE_URL', 'MONGODB_URI', 'REDIS_URL'), , indent=4)

Render the `on.workflow_call.secrets:` body for the reusable workflow.

Every secret is declared `required: false` — the publish job enforces
`PYPI_PASSWORD` at run time with a clear message, and the export action
enforces any repo-declared `required_envvars`.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> print(render_workflow_call_secrets(["PYPI_PASSWORD", "OPENAI_API_KEY"]))
    PYPI_PASSWORD:
      required: false
    OPENAI_API_KEY:
      required: false
```

### wads.ci_secrets.stub_transport(stub)

The secrets transport of a stub’s content: `"json"` or `"named"`.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> stub_transport("    secrets:\n" + render_stub_json_transport() + "\n")
'json'
>>> stub_transport("    secrets:\n      PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }}\n")
'named'
```

### wads.ci_secrets.stub_with_named_transport(stub, names)

Rewrite a stub’s secrets region to the named transport, passing `names`.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> stub = "jobs:\n  ci:\n    secrets:\n" + render_stub_json_transport() + "\n"
>>> stub_with_named_transport(stub, ["PYPI_PASSWORD"]).endswith(
...     "      PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }}\n")
True
```

### wads.ci_secrets.stub_with_transport(stub, , transport='named', names=('PYPI_PASSWORD',))

Rewrite a stub’s secrets region to the given transport.

Works on a stub of either transport, so it converts JSON to named, named
to JSON, and re-lists a named stub’s names; the comment lines directly
above `secrets:` are replaced too, so a stub never describes a transport
it does not use. `names` is used by the named transport only. A stub
whose `ci` job has no `secrets:` mapping block (`secrets: inherit`,
say) raises `ValueError`.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> json_stub = (
...     "jobs:\n  ci:\n    uses: x\n    # Transport: whole context.\n"
...     "    secrets:\n" + render_stub_json_transport() + "\n"
... )
>>> named = stub_with_transport(json_stub, names=["PYPI_PASSWORD", "HF_TOKEN"])
>>> named.endswith("      HF_TOKEN: ${{ secrets.HF_TOKEN }}\n")
True
>>> "toJSON" in named, stub_transport(named)
(False, 'named')
>>> back = stub_with_transport(named, transport="json")
>>> stub_transport(back), "HF_TOKEN" in back
('json', False)
>>> stub_with_transport(back, names=["PYPI_PASSWORD"]) == stub_with_transport(
...     json_stub, names=["PYPI_PASSWORD"])
True
```

### wads.ci_secrets.warn_names_outside_superset(names)

Warn loudly for names a named-transport stub cannot legally pass.

A caller may only pass secrets the reusable workflow declares. With
`transport="named"` that universe is the superset in
[`wads.ci_secrets.DEFAULT_CI_SECRETS`](_autosummary/wads.ci_secrets.html.md#wads.ci_secrets.DEFAULT_CI_SECRETS); a stub naming anything outside
it produces a workflow GitHub rejects at parse time — zero jobs, an opaque
`startup_failure` (issue #63). Returns the offending names.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)


# _autosummary/wads.ci_trigger.html.md

# wads.ci_trigger

On-demand CI: render a repo’s caller stub from `[tool.wads.ci.trigger]`, and flip
a repo to on-demand in one idempotent command.

`[tool.wads.ci.trigger].mode` decides whether CI runs at all:

- `"auto"` (the default): every push and pull request, as wads always has. The stub
  renders byte-identical to `wads/data/github_ci_uv_stub.yml`.
- `"on-demand"`: **nothing runs unless asked** – the commit subject carries
  `run_ci_marker` (default `"[run ci]"`), or someone starts a `workflow_dispatch`.

Two gates, because workflow expressions cannot split a string:

1. The stub’s job-level `if:` is a zero-cost pre-filter. An ordinary push schedules
   no runner at all, but the pre-filter can only ask whether the marker is *anywhere*
   in the commit message.
2. The reusable workflow’s setup job extracts the subject line, and every other job
   gates on the marker being in *that* (the TRIGGER GATE note in
   `.github/workflows/uv-ci.yml`). A marker quoted only in a squash-merged PR body
   passes gate 1, costs one setup job, and runs nothing else.

The flip ([`flip_to_on_demand()`](_autosummary/wads.ci_trigger.html.md#wads.ci_trigger.flip_to_on_demand), CLI `wads-migrate ci-on-demand`) sets the mode
plus a cheap test matrix and re-renders the stub. Everything is computed in a shadow
copy of the repo first, so a dry run and a real run take the same path.

### Functions

| [`classify_ci_workflow`](_autosummary/wads.ci_trigger.html.md#wads.ci_trigger.classify_ci_workflow)(text)                      | Classify a `ci.yml`: `'missing'`, `'stub'`, `'inline-uv'` or `'other'`.         |
|--------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------|
| [`commit_paths`](_autosummary/wads.ci_trigger.html.md#wads.ci_trigger.commit_paths)(repo, paths, \*, message)          | Commit exactly `paths` and return the new commit's short sha.                   |
| [`flip_to_on_demand`](_autosummary/wads.ci_trigger.html.md#wads.ci_trigger.flip_to_on_demand)([repo, python_versions, ...]) | Flip a repo to on-demand CI: pyproject settings plus a re-rendered stub.        |
| [`render_stub_inputs`](_autosummary/wads.ci_trigger.html.md#wads.ci_trigger.render_stub_inputs)(stub, inputs)                | Add a `with:` block (the reusable workflow's inputs) under the stub's `uses:`.  |
| [`render_stub_trigger`](_autosummary/wads.ci_trigger.html.md#wads.ci_trigger.render_stub_trigger)(stub, \*[, mode, ...])      | Render the caller stub for a trigger mode.                                      |
| [`repo_pyproject_for`](_autosummary/wads.ci_trigger.html.md#wads.ci_trigger.repo_pyproject_for)(ci_path)                     | The `pyproject.toml` of the repo holding workflow file `ci_path`, if any.       |
| [`run_ci_on_demand`](_autosummary/wads.ci_trigger.html.md#wads.ci_trigger.run_ci_on_demand)([repo, dry_run, ...])          | `wads-migrate ci-on-demand`: flip, report, optionally commit.                   |
| [`set_on_demand_in_pyproject`](_autosummary/wads.ci_trigger.html.md#wads.ci_trigger.set_on_demand_in_pyproject)(text, \*[, ...])     | Return pyproject.toml `text` with the on-demand settings applied.               |
| [`stub_customizations`](_autosummary/wads.ci_trigger.html.md#wads.ci_trigger.stub_customizations)(text)                       | Split what a stub holds beyond the template into `(with_inputs, dropped)`.      |
| [`stub_if_expression`](_autosummary/wads.ci_trigger.html.md#wads.ci_trigger.stub_if_expression)([run_ci_marker])             | The on-demand stub's job-level pre-filter, as a workflow expression.            |
| [`stub_inputs_of`](_autosummary/wads.ci_trigger.html.md#wads.ci_trigger.stub_inputs_of)(ci_path)                         | The `with:` inputs an existing stub file passes; `{}` if none or not a stub.    |
| [`stub_shape`](_autosummary/wads.ci_trigger.html.md#wads.ci_trigger.stub_shape)(ci_text)                             | The `pin` (which a re-render keeps) and secrets `transport` of a stub.          |
| [`trigger_for_workflow`](_autosummary/wads.ci_trigger.html.md#wads.ci_trigger.trigger_for_workflow)(ci_path)                   | The `(mode, run_ci_marker)` declared by the repo holding `ci_path`.             |
| [`unasked_workflows`](_autosummary/wads.ci_trigger.html.md#wads.ci_trigger.unasked_workflows)(repo, \*[, workflow])         | Other workflow files in `repo` that run without anyone asking (push, PR, cron). |
| [`uncommitted`](_autosummary/wads.ci_trigger.html.md#wads.ci_trigger.uncommitted)(repo, paths)                        | Which of `paths` have uncommitted changes (`git status --porcelain` lines).     |
| [`validate_trigger`](_autosummary/wads.ci_trigger.html.md#wads.ci_trigger.validate_trigger)(mode, run_ci_marker)           | Validate a (mode, marker) pair with the same rules the CI setup job applies.    |

### Classes

| [`FlipResult`](_autosummary/wads.ci_trigger.html.md#wads.ci_trigger.FlipResult)(repo[, status, changes, notes])   | What [`flip_to_on_demand()`](_autosummary/wads.ci_trigger.html.md#wads.ci_trigger.flip_to_on_demand) changed or, on a dry run, would change.   |
|-----------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------|

### *class* wads.ci_trigger.FlipResult(repo, status='unchanged', changes=<factory>, notes=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

What [`flip_to_on_demand()`](_autosummary/wads.ci_trigger.html.md#wads.ci_trigger.flip_to_on_demand) changed or, on a dry run, would change.

`status` is `"changed"`, `"unchanged"` (already on-demand) or `"refused"`
(nothing written; `notes` says why). `changes` maps a repo-relative path to its
`(old, new)` text.

#### diff()

Unified diff of every change.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### wads.ci_trigger.classify_ci_workflow(text)

Classify a `ci.yml`: `'missing'`, `'stub'`, `'inline-uv'` or `'other'`.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> classify_ci_workflow(None)
'missing'
>>> classify_ci_workflow("uses: i2mint/wads/.github/workflows/uv-ci.yml@master")
'stub'
>>> classify_ci_workflow("uses: i2mint/wads/actions/run-tests-uv@master")
'inline-uv'
>>> classify_ci_workflow("uses: actions/setup-python@v5")
'other'
```

### wads.ci_trigger.commit_paths(repo, paths, , message)

Commit exactly `paths` and return the new commit’s short sha.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### wads.ci_trigger.flip_to_on_demand(repo='.', , python_versions=('3.12',), test_on_windows=False, run_ci_marker=None, workflow='.github/workflows/ci.yml', dry_run=False)

Flip a repo to on-demand CI: pyproject settings plus a re-rendered stub.

- a **stub** `ci.yml` is re-rendered keeping its pin and `with:` inputs
  (secrets move to the named transport, as `wads-migrate ci-to-stub`
  does), or **refused** if it holds anything else a re-render would drop
  (extra jobs, custom triggers);
- an **inline uv** workflow becomes the stub, carrying its secret-backed env vars
  into `[tool.wads.ci.env]` first (as `wads-migrate ci-to-stub` does);
- **no** `ci.yml`: only pyproject changes (nothing runs on push anyway);
- anything else (2025/legacy/custom workflow) is **refused** and nothing is
  written, because it would ignore the setting and keep running on every push.

Idempotent: a second run reports `"unchanged"` and writes nothing.

* **Return type:**
  [`FlipResult`](_autosummary/wads.ci_trigger.html.md#wads.ci_trigger.FlipResult)

### wads.ci_trigger.render_stub_inputs(stub, inputs)

Add a `with:` block (the reusable workflow’s inputs) under the stub’s `uses:`.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### wads.ci_trigger.render_stub_trigger(stub, , mode='auto', run_ci_marker='[run ci]')

Render the caller stub for a trigger mode.

`"auto"` returns `stub` unchanged. `"on-demand"` replaces the push/PR trigger
with push + `workflow_dispatch`, puts the plain-language explanation above it, and
adds the pre-filter `if:` to the `ci` job.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### wads.ci_trigger.repo_pyproject_for(ci_path)

The `pyproject.toml` of the repo holding workflow file `ci_path`, if any.

Walks up from the workflow, stopping at the first directory that holds a `.git`
so an enclosing project’s pyproject is never mistaken for this repo’s.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)]

### wads.ci_trigger.run_ci_on_demand(repo='.', , dry_run=False, python_versions=('3.12',), test_on_windows=False, run_ci_marker=None, workflow='.github/workflows/ci.yml', commit=False, out=None, err=None)

`wads-migrate ci-on-demand`: flip, report, optionally commit. Returns an exit code.

`0` changed or already on-demand, `2` refused (nothing written).

* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int)

### wads.ci_trigger.set_on_demand_in_pyproject(text, , python_versions=('3.12',), test_on_windows=False, run_ci_marker=None)

Return pyproject.toml `text` with the on-demand settings applied.

Sets `[tool.wads.ci.trigger].mode = "on-demand"` (and `run_ci_marker` when
given), `[tool.wads.ci.testing].python_versions` and `test_on_windows`.
Comments and layout survive (tomlkit), and a key already holding its target value
is not touched, so applying this twice returns the first result unchanged.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### wads.ci_trigger.stub_customizations(text)

Split what a stub holds beyond the template into `(with_inputs, dropped)`.

`with_inputs` (a flat `with:` mapping, e.g. `project-name`) survives a
re-render; `dropped` names everything else a re-render would silently lose.
Raises `yaml.YAMLError` when the text does not parse.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict), [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)]

```pycon
>>> stub_customizations(
...     "on: [push, pull_request]\njobs:\n  ci:\n    uses: x\n"
...     "    with:\n      project-name: my_pkg\n"
... )
({'project-name': 'my_pkg'}, [])
>>> stub_customizations("on: [push]\njobs:\n  ci:\n    uses: x\n  lint:\n    runs-on: y\n")[1]
['`on: ["push"]`', 'job `lint`']
```

### wads.ci_trigger.stub_if_expression(run_ci_marker='[run ci]')

The on-demand stub’s job-level pre-filter, as a workflow expression.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> stub_if_expression("[run ci]")
"github.event_name == 'workflow_dispatch' || contains(github.event.head_commit.message, '[run ci]')"
```

A quote in the marker is doubled, which is how workflow expressions escape it:

```pycon
>>> stub_if_expression("it's")
"github.event_name == 'workflow_dispatch' || contains(github.event.head_commit.message, 'it''s')"
```

### wads.ci_trigger.stub_inputs_of(ci_path)

The `with:` inputs an existing stub file passes; `{}` if none or not a stub.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### wads.ci_trigger.stub_shape(ci_text)

The `pin` (which a re-render keeps) and secrets `transport` of a stub.

Defaults (`@master`, `named`) for anything that is not a stub. A
re-render writes the named transport whatever this reports
(i2mint/wads#74, #101); see [`wads.migration.migrate_ci_to_stub()`](_autosummary/wads.migration.html.md#wads.migration.migrate_ci_to_stub).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

```pycon
>>> stub_shape("uses: i2mint/wads/.github/workflows/uv-ci.yml@0.2.30")
{'pin': '@0.2.30', 'transport': 'named'}
>>> stub_shape(None)
{'pin': '@master', 'transport': 'named'}
```

### wads.ci_trigger.trigger_for_workflow(ci_path)

The `(mode, run_ci_marker)` declared by the repo holding `ci_path`.

Defaults when there is no such file or no pyproject. An invalid trigger table
raises: a stub must never silently render `auto` for a repo that asked for
`on-demand`.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### wads.ci_trigger.unasked_workflows(repo, , workflow='.github/workflows/ci.yml')

Other workflow files in `repo` that run without anyone asking (push, PR, cron).

The flip only governs `workflow`; these keep running and are worth knowing about.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### wads.ci_trigger.uncommitted(repo, paths)

Which of `paths` have uncommitted changes (`git status --porcelain` lines).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### wads.ci_trigger.validate_trigger(mode, run_ci_marker)

Validate a (mode, marker) pair with the same rules the CI setup job applies.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> validate_trigger("on-demand", "[run ci]")
('on-demand', '[run ci]')
>>> validate_trigger("sometimes", "[run ci]")
Traceback (most recent call last):
  ...
ValueError: [tool.wads.ci.trigger].mode must be one of ('auto', 'on-demand'), got 'sometimes'
```


# _autosummary/wads.config_comparison.html.md

# wads.config_comparison

Compare project configuration files (pyproject.toml, setup.cfg, MANIFEST.in) against templates.

This module provides tools to analyze project configurations and identify
differences from standard templates, helping users maintain up-to-date
project structures.

Key Functions:
: compare_pyproject_toml: Compare actual pyproject.toml against template
  compare_setup_cfg: Analyze setup.cfg and recommend migration
  compare_manifest_in: Analyze MANIFEST.in and recommend Hatchling migration
  summarize_config_status: Overall project config health check
  compare_ci_workflow: Compare CI workflow against template

### Example

```pycon
>>> from wads.config_comparison import summarize_config_status
>>> status = summarize_config_status('/path/to/project')
>>> if status['needs_attention']:
...     print(status['recommendations'])
```

### Functions

| [`compare_ci_workflow`](_autosummary/wads.config_comparison.html.md#wads.config_comparison.compare_ci_workflow)(actual_path[, ...])     | Compare CI workflow against modern template.                            |
|----------------------------------------------------------------------------------------------|-------------------------------------------------------------------------|
| [`compare_manifest_in`](_autosummary/wads.config_comparison.html.md#wads.config_comparison.compare_manifest_in)(actual_path)            | Analyze MANIFEST.in and recommend migration to Hatchling configuration. |
| [`compare_pyproject_toml`](_autosummary/wads.config_comparison.html.md#wads.config_comparison.compare_pyproject_toml)(actual_path[, ...])  | Compare actual pyproject.toml against template.                         |
| [`compare_setup_cfg`](_autosummary/wads.config_comparison.html.md#wads.config_comparison.compare_setup_cfg)(actual_path, \*[, ...])   | Analyze setup.cfg and recommend migration to pyproject.toml.            |
| [`summarize_config_status`](_autosummary/wads.config_comparison.html.md#wads.config_comparison.summarize_config_status)(pkg_dir, \*[, ...]) | Check overall config status of a project.                               |

### wads.config_comparison.compare_ci_workflow(actual_path, template_path='/home/runner/work/wads/wads/wads/data/github_ci_publish_2025.yml', , project_name=None)

Compare CI workflow against modern template.

* **Parameters:**
  * **actual_path** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)) – Path to .github/workflows/ci.yml
  * **template_path** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)) – Path to template CI workflow
  * **project_name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Optional project name for placeholder replacement
* **Return type:**
  [`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]
* **Returns:**
  Dictionary with comparison results and recommendations

### Example

```pycon
>>> diff = compare_ci_workflow('.github/workflows/ci.yml')
>>> if diff['needs_attention']:
...     print("CI might be outdated")
```

### wads.config_comparison.compare_manifest_in(actual_path)

Analyze MANIFEST.in and recommend migration to Hatchling configuration.

* **Parameters:**
  **actual_path** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)) – Path to MANIFEST.in file
* **Return type:**
  [`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]
* **Returns:**
  Dictionary with migration recommendations

### Example

```pycon
>>> analysis = compare_manifest_in('MANIFEST.in')
>>> if analysis['needs_migration']:
...     print(analysis['hatchling_config'])
```

### wads.config_comparison.compare_pyproject_toml(actual_path, template_path='/home/runner/work/wads/wads/wads/data/pyproject_toml_tpl.toml', , ignore_keys=None, project_name=None)

Compare actual pyproject.toml against template.

* **Parameters:**
  * **actual_path** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)) – Path to the project’s pyproject.toml
  * **template_path** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)) – Path to template pyproject.toml
  * **ignore_keys** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Set`](https://docs.python.org/3/library/typing.html#typing.Set)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]) – Keys to ignore in comparison (project-specific values)
  * **project_name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Optional project name for contextual recommendations
* **Returns:**
  - ‘missing_sections’: sections in template but not in actual
  - ’outdated_sections’: sections in actual that might be outdated
  - ’recommendations’: suggested updates
  - ’needs_attention’: boolean flag
* **Return type:**
  [`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### Example

```pycon
>>> diff = compare_pyproject_toml('my_project/pyproject.toml')
>>> if diff['needs_attention']:
...     for rec in diff['recommendations']:
...         print(f"  - {rec}")
```

### wads.config_comparison.compare_setup_cfg(actual_path, , warn_about_deprecation=True)

Analyze setup.cfg and recommend migration to pyproject.toml.

* **Parameters:**
  * **actual_path** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)) – Path to setup.cfg file
  * **warn_about_deprecation** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Whether to warn about using deprecated format
* **Return type:**
  [`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]
* **Returns:**
  Dictionary with migration recommendations

### Example

```pycon
>>> analysis = compare_setup_cfg('old_project/setup.cfg')
>>> if analysis['should_migrate']:
...     print(analysis['recommendations'])
```

### wads.config_comparison.summarize_config_status(pkg_dir, , check_ci=True, project_name=None)

Check overall config status of a project.

Analyzes pyproject.toml, setup.cfg, CI workflows and returns a comprehensive
summary of what needs attention.

* **Parameters:**
  * **pkg_dir** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)) – Path to project directory
  * **check_ci** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Whether to check CI workflow
  * **project_name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Optional project name for contextual checks
* **Returns:**
  - ‘has_pyproject’: bool
  - ’has_setup_cfg’: bool
  - ’has_ci’: bool
  - ’needs_attention’: list of issues
  - ’recommendations’: list of recommendations
  - Details for each file type
* **Return type:**
  [`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### Example

```pycon
>>> status = summarize_config_status('/path/to/project')
>>> for issue in status['needs_attention']:
...     print(f"⚠️  {issue}")
```


# _autosummary/wads.fleet_migrate.html.md

# wads.fleet_migrate

Batch migration helpers across the user’s local Python ecosystem.

Single entry point: `fleet_stub` (also exposed as `wads-migrate fleet-stub`)
which converts a batch of repos to the SSOT stub CI workflow in one call.

Reads the candidate list from the wads-ci-sweep state file
(`~/Downloads/wads_ci_diagnosis.json`), selects by category, orders by
last-commit recency, and applies `migrate_ci_to_stub` to each. Per-repo
failures are isolated — the batch never aborts midway.

See: wads-ci-sweep skill at `~/.claude/skills/wads-ci-sweep/` for the
ecosystem inventory + state-tracking it relies on.

### Functions

| [`fleet_stub`](_autosummary/wads.fleet_migrate.html.md#wads.fleet_migrate.fleet_stub)(\*[, limit, select_category, ...])   | Stub-migrate up to `limit` repos, picked from the sweep state file.   |
|--------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------|
| [`migrate_one_to_stub`](_autosummary/wads.fleet_migrate.html.md#wads.fleet_migrate.migrate_one_to_stub)(repo_path, \*[, pin, ...])  | Stub-migrate one repo end-to-end: sync, rewrite, commit, push.        |
| [`read_sweep_state`](_autosummary/wads.fleet_migrate.html.md#wads.fleet_migrate.read_sweep_state)([path])                        | Load the wads-ci-sweep state file.                                    |
| [`select_candidates`](_autosummary/wads.fleet_migrate.html.md#wads.fleet_migrate.select_candidates)(state, \*[, category, ...])   | Pick which packages to migrate, in priority order.                    |

### Classes

| [`RepoResult`](_autosummary/wads.fleet_migrate.html.md#wads.fleet_migrate.RepoResult)(name, path, status[, detail])   | Outcome of attempting to stub-migrate one repo.   |
|---------------------------------------------------------------------------------------------|---------------------------------------------------|

### *class* wads.fleet_migrate.RepoResult(name, path, status, detail='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Outcome of attempting to stub-migrate one repo.

### wads.fleet_migrate.fleet_stub(, limit=20, select_category='uv_current', state_file=PosixPath('/home/runner/Downloads/wads_ci_diagnosis.json'), dry_run=False, pin='@master', commit_message='ci: switch to wads reusable workflow stub', order_by='git_recency')

Stub-migrate up to `limit` repos, picked from the sweep state file.

* **Parameters:**
  * **limit** ([`int`](https://docs.python.org/3/builtins/functions.html#int)) – Max repos to migrate this run.
  * **select_category** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Only consider packages with this `category` in the
    sweep state file (default `"uv_current"`).
  * **state_file** ([`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) | [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Path to the wads-ci-sweep JSON. Default
    `~/Downloads/wads_ci_diagnosis.json`.
  * **dry_run** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – If True, print candidates and exit without touching anything.
  * **pin** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Wads ref the stub points at (passed to `migrate_ci_to_stub`).
  * **commit_message** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Commit message for the per-repo stub-conversion commit.
  * **order_by** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Candidate ordering — `"git_recency"` or `"name"`.
* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`RepoResult`](_autosummary/wads.fleet_migrate.html.md#wads.fleet_migrate.RepoResult)]
* **Returns:**
  List of [`RepoResult`](_autosummary/wads.fleet_migrate.html.md#wads.fleet_migrate.RepoResult), one per attempted repo (empty for dry-run).

### wads.fleet_migrate.migrate_one_to_stub(repo_path, , pin='@master', commit_message='ci: switch to wads reusable workflow stub', workflow_path='.github/workflows/ci.yml')

Stub-migrate one repo end-to-end: sync, rewrite, commit, push.

Idempotent: if the workflow is already on the latest stub template, returns
`status="noop"`.

* **Return type:**
  [`RepoResult`](_autosummary/wads.fleet_migrate.html.md#wads.fleet_migrate.RepoResult)

### wads.fleet_migrate.read_sweep_state(path=PosixPath('/home/runner/Downloads/wads_ci_diagnosis.json'))

Load the wads-ci-sweep state file.

* **Raises:**
  [**FileNotFoundError**](https://docs.python.org/3/builtins/exceptions.html#FileNotFoundError) – if the state file does not exist (the caller likely
      needs to run `python ~/.claude/skills/wads-ci-sweep/sweep.py
      diagnose` to create it).
* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### wads.fleet_migrate.select_candidates(state, , category='uv_current', limit=20, order_by='git_recency')

Pick which packages to migrate, in priority order.

* **Parameters:**
  * **state** ([`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)) – Sweep state dict (from [`read_sweep_state()`](_autosummary/wads.fleet_migrate.html.md#wads.fleet_migrate.read_sweep_state)).
  * **category** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Only consider packages with this `category`. Default
    `"uv_current"` — repos already on inline uv-CI that can safely
    move to the stub. Other useful values: `"uv_stub"` (already
    done; useful for refresh sweeps after a stub-template change).
  * **limit** ([`int`](https://docs.python.org/3/builtins/functions.html#int)) – Max number of repos to return.
  * **order_by** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – `"git_recency"` (default) sorts most-recently-committed
    first. `"name"` sorts alphabetically.
* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]
* **Returns:**
  List of package records (subset of `state['packages']`), in the
  chosen order.


# _autosummary/wads.html.md

# wads

## Console Scripts

To see available commands

```default
wads --help
```

### Functions

| `pkg_join`(\*paths)   |    |
|-----------------------|----|
| `rjoin`(\*paths)      |    |

### Modules

| [`agents`](_autosummary/wads.agents.html.md#module-wads.agents)                           | Wads AI Agents                                                                                                                   |
|------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------|
| [`ci_config`](_autosummary/wads.ci_config.html.md#module-wads.ci_config)                     | Utilities for reading and applying CI configuration from pyproject.toml.                                                         |
| [`ci_local`](_autosummary/wads.ci_local.html.md#module-wads.ci_local)                       | Do locally what the wads CI would have done: the same `[tool.wads.ci]`, no Actions minutes.                                      |
| [`ci_secrets`](_autosummary/wads.ci_secrets.html.md#module-wads.ci_secrets)                   | Canonical registry of CI secret names for wads-managed projects.                                                                 |
| [`ci_trigger`](_autosummary/wads.ci_trigger.html.md#module-wads.ci_trigger)                   | On-demand CI: render a repo's caller stub from `[tool.wads.ci.trigger]`, and flip a repo to on-demand in one idempotent command. |
| [`config_comparison`](_autosummary/wads.config_comparison.html.md#module-wads.config_comparison)     | Compare project configuration files (pyproject.toml, setup.cfg, MANIFEST.in) against templates.                                  |
| [`fleet_migrate`](_autosummary/wads.fleet_migrate.html.md#module-wads.fleet_migrate)             | Batch migration helpers across the user's local Python ecosystem.                                                                |
| [`install_skills`](_autosummary/wads.install_skills.html.md#module-wads.install_skills)           | Install wads Claude Code skills to ~/.claude/skills/ for global availability.                                                    |
| [`install_system_deps`](_autosummary/wads.install_system_deps.html.md#module-wads.install_system_deps) | Install system dependencies from [tool.wads.ops.\*] sections in pyproject.toml.                                                  |
| [`licence_check`](_autosummary/wads.licence_check.html.md#module-wads.licence_check)             | Audit the licence perimeter of a package's installed dependency closure.                                                         |
| [`licensing`](_autosummary/wads.licensing.html.md#module-wads.licensing)                     | Licensing                                                                                                                        |
| [`migration`](_autosummary/wads.migration.html.md#module-wads.migration)                     | Migration tools for converting old setuptools/CI configurations to modern formats.                                               |
| [`npm_config`](_autosummary/wads.npm_config.html.md#module-wads.npm_config)                   | Read NPM CI configuration from a `package.json` `wads.ci` block.                                                                 |
| [`pack`](_autosummary/wads.pack.html.md#module-wads.pack)                               | Utils to package and publish.                                                                                                    |
| [`populate`](_autosummary/wads.populate.html.md#module-wads.populate)                       | Populate a package directory with useful packaging files.                                                                        |
| [`profiles`](_autosummary/wads.profiles.html.md#module-wads.profiles)                       | Declarative generation *profiles* and *overlays* built on the engine.                                                            |
| [`project_setup`](_autosummary/wads.project_setup.html.md#module-wads.project_setup)             | Project setup utilities: name checking, GitHub operations, and orchestration.                                                    |
| [`repo_audit`](_autosummary/wads.repo_audit.html.md#module-wads.repo_audit)                   | Read-only health audit of a (wads-managed) Python repo.                                                                          |
| [`scripts`](_autosummary/wads.scripts.html.md#module-wads.scripts)                         | CI and automation scripts for wads.                                                                                              |
| [`secrets_cli`](_autosummary/wads.secrets_cli.html.md#module-wads.secrets_cli)                 | `wads-secrets` — manage the CI secrets/env vars of a wads-managed repo.                                                          |
| [`setup_utils`](_autosummary/wads.setup_utils.html.md#module-wads.setup_utils)                 | Utilities for setting up packages based on pyproject.toml configuration.                                                         |
| [`templating`](_autosummary/wads.templating.html.md#module-wads.templating)                   | Declarative, template-source-driven generation engine.                                                                           |
| [`toml_util`](_autosummary/wads.toml_util.html.md#module-wads.toml_util)                     | Utilities for reading and writing pyproject.toml files.                                                                          |
| [`user_dirs`](_autosummary/wads.user_dirs.html.md#module-wads.user_dirs)                     | Platform-appropriate user directories for wads configuration and data.                                                           |
| [`util`](_autosummary/wads.util.html.md#module-wads.util)                               | wads util                                                                                                                        |


# _autosummary/wads.install_skills.html.md

# wads.install_skills

Install wads Claude Code skills to ~/.claude/skills/ for global availability.

Skills shipped with wads:

- setup-py-project: AI-assisted Python project creation
- wads-migrate: Migration to modern wads/uv setup

Usage:

```default
wads-install-skills          # Install all skills (symlinks by default)
wads-install-skills --list   # List available skills
wads-install-skills --force  # Overwrite existing skills
wads-install-skills --copy   # Copy files instead of symlinking
```

### Functions

| [`install_skills`](_autosummary/wads.install_skills.html.md#wads.install_skills.install_skills)(\*[, force, copy, verbose])   | Install wads Claude Code skills to ~/.claude/skills/.   |
|-----------------------------------------------------------------------------------------------|---------------------------------------------------------|
| [`list_available_skills`](_autosummary/wads.install_skills.html.md#wads.install_skills.list_available_skills)()                      | List skill names bundled with wads.                     |
| [`main`](_autosummary/wads.install_skills.html.md#wads.install_skills.main)()                                       | CLI entry point for wads-install-skills.                |

### wads.install_skills.install_skills(, force=False, copy=False, verbose=True)

Install wads Claude Code skills to ~/.claude/skills/.

By default, creates symlinks so skills stay in sync with the wads package.
Use `copy=True` to copy files instead (not recommended — updates won’t
propagate, and the skills depend on wads being installed anyway).

* **Parameters:**
  * **force** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – If True, overwrite existing skill entries.
  * **copy** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – If True, copy files instead of creating symlinks.
  * **verbose** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – If True, print progress.
* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]
* **Returns:**
  List of installed skill names.

### wads.install_skills.list_available_skills()

List skill names bundled with wads.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### wads.install_skills.main()

CLI entry point for wads-install-skills.


# _autosummary/wads.install_system_deps.html.md

# wads.install_system_deps

Install system dependencies from [tool.wads.ops.\*] sections in pyproject.toml.

This script reads system dependency configurations and installs them on the appropriate platform.
It can be used as a standalone CLI tool or imported as a module.

Each `[tool.wads.ops.<dep>]` section may declare an `install_timeout` (seconds)
to raise the per-command ceiling for a dependency that is slow but healthy — a
cold-cache `apt-get install ffmpeg` on a GitHub runner is the motivating case.
Without a declaration the timeout stays at `DFLT_INSTALL_TIMEOUT`:

```default
[tool.wads.ops.ffmpeg]
install_timeout = 900
install.linux = ["sudo apt-get update", "sudo apt-get install -y ffmpeg"]
```

The CLI’s `--install-timeout` sets the fallback for dependencies that declare
none; a declaration always wins over it.

### Functions

| [`check_if_installed`](_autosummary/wads.install_system_deps.html.md#wads.install_system_deps.check_if_installed)(dep_name, check_cmds[, ...])   | Check if dependency is already installed using check commands.   |
|----------------------------------------------------------------------------------------------------|------------------------------------------------------------------|
| [`detect_platform`](_autosummary/wads.install_system_deps.html.md#wads.install_system_deps.detect_platform)()                                 | Detect the current platform.                                     |
| [`find_pyproject`](_autosummary/wads.install_system_deps.html.md#wads.install_system_deps.find_pyproject)(path)                              | Find pyproject.toml file from path (file or directory).          |
| [`install_dependency`](_autosummary/wads.install_system_deps.html.md#wads.install_system_deps.install_dependency)(dep_name, install_cmds[, ...]) | Install a dependency using install commands.                     |
| [`install_system_dependencies`](_autosummary/wads.install_system_deps.html.md#wads.install_system_deps.install_system_dependencies)([...])                | Install system dependencies from pyproject.toml.                 |
| [`main`](_autosummary/wads.install_system_deps.html.md#wads.install_system_deps.main)()                                            | CLI entry point.                                                 |
| [`read_system_deps`](_autosummary/wads.install_system_deps.html.md#wads.install_system_deps.read_system_deps)(pyproject_path)                  | Read [tool.wads.ops.\*] sections from pyproject.toml.            |

### wads.install_system_deps.check_if_installed(dep_name, check_cmds, timeout=10)

Check if dependency is already installed using check commands.

* **Parameters:**
  * **dep_name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Name of dependency
  * **check_cmds** (`any`) – Command(s) to check if installed (string, list, or empty)
  * **timeout** ([`float`](https://docs.python.org/3/builtins/functions.html#float)) – Timeout in seconds for check commands
* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)
* **Returns:**
  True if installed, False otherwise

### wads.install_system_deps.detect_platform()

Detect the current platform.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### wads.install_system_deps.find_pyproject(path)

Find pyproject.toml file from path (file or directory).

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### wads.install_system_deps.install_dependency(dep_name, install_cmds, timeout=300)

Install a dependency using install commands.

* **Parameters:**
  * **dep_name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Name of dependency
  * **install_cmds** (`any`) – Command(s) to install (string or list)
  * **timeout** ([`float`](https://docs.python.org/3/builtins/functions.html#float)) – Timeout in seconds for each command
* **Returns:**
  bool, error_message: Optional[str])
* **Return type:**
  [`Tuple`](https://docs.python.org/3/library/typing.html#typing.Tuple)[[`bool`](https://docs.python.org/3/builtins/functions.html#bool), [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]

### wads.install_system_deps.install_system_dependencies(pyproject_path='.', platform=None, skip_check=False, verbose=True, , default_install_timeout=300)

Install system dependencies from pyproject.toml.

* **Parameters:**
  * **pyproject_path** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Path to pyproject.toml or directory containing it
  * **platform** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – Platform to install for (linux/macos/windows), auto-detect if None
  * **skip_check** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Skip checking if dependencies are already installed
  * **verbose** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Print detailed progress
  * **default_install_timeout** ([`float`](https://docs.python.org/3/builtins/functions.html#float)) – Per-command install timeout (seconds) for
    dependencies that declare no `install_timeout` of their own.
* **Return type:**
  [`Tuple`](https://docs.python.org/3/library/typing.html#typing.Tuple)[[`int`](https://docs.python.org/3/builtins/functions.html#int), [`int`](https://docs.python.org/3/builtins/functions.html#int), [`int`](https://docs.python.org/3/builtins/functions.html#int)]
* **Returns:**
  (installed_count, skipped_count, failed_count)

### wads.install_system_deps.main()

CLI entry point.

### wads.install_system_deps.read_system_deps(pyproject_path)

Read [tool.wads.ops.\*] sections from pyproject.toml.

* **Return type:**
  [`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]


# _autosummary/wads.licence_check.html.md

# wads.licence_check

Audit the licence perimeter of a package’s installed dependency closure.

A licence rule nobody checks is a sentence, not a policy. This module walks the
**installed** metadata of everything a bare `pip install <pkg>` pulls in and
reports every distribution whose declared licence falls outside the perimeter.

Run it from the command line:

```default
wads-licence-check                      # the project in the current directory
wads-licence-check path/to/project
wads-licence-check . --python .venv/bin/python   # read ANOTHER environment
```

Exit code is `1` when the perimeter is breached, `0` when it holds.

Configure it in the audited project’s `pyproject.toml`:

```default
[tool.wads.licence]
enabled = true                # read by wads.ci_config for the CI gate
allowed = ["MIT", "BSD", "Apache-2.0", "ISC"]
forbidden = ["AGPL", "GPL", "LGPL", "SSPL", "BUSL", "Elastic-2.0", "RAIL"]
include-extras = []           # [] = hard deps only; ["*"] = every extra
unknown-is-failure = true
unclassified-is-failure = false

[tool.wads.licence.exceptions]
certifi = "MPL-2.0 - file-level weak copyleft over an unmodified CA bundle."
```

`allowed` and `forbidden` REPLACE the defaults, they do not extend them, so
a hand-written list is a narrowing unless it is a superset of
[`DFLT_ALLOWED`](_autosummary/wads.licence_check.html.md#wads.licence_check.DFLT_ALLOWED) / [`DFLT_FORBIDDEN`](_autosummary/wads.licence_check.html.md#wads.licence_check.DFLT_FORBIDDEN). [`self_check()`](_autosummary/wads.licence_check.html.md#wads.licence_check.self_check) refuses to run
a policy that has narrowed away a whole licence family, which is the guard that
makes this survivable – but the honest move is to start from the defaults.

Write patterns as TOML **literal** strings (single quotes):

```default
forbidden = ['\bGPL', '\bLGPL']      # right
forbidden = ["\bGPL", "\bLGPL"]      # WRONG: \b is a backspace character
```

TOML basic strings process escapes, so `"\bGPL"` reaches the regex engine as
`"\x08GPL"` and silently matches nothing at all.

Exit codes: `0` the perimeter holds, `1` it is breached, `2` the tool could
not run (bad config, unreadable environment, a policy that cannot detect).

Three properties are load-bearing, and each has a recorded failure behind it.

**1. The precision ladder** ([`declare()`](_autosummary/wads.licence_check.html.md#wads.licence_check.declare)). Read, in order, the PEP 639
`License-Expression`, then the `License ::` trove classifiers, and only then
the **first line** of the free-text `License` field. Never substring-scan that
field whole: numpy is BSD-3-Clause and its field carries an LGPL URL for a
vendored component’s notice, so a naive scan reports numpy as copyleft. A
one-field check is wrong in both directions: `click` declares a PEP 639
expression and **no** classifiers, `i2` declares neither and only a free-text
field, and Arize Phoenix declares Elastic-2.0 in the field with no classifier at
all – so a classifier-only gate sails straight past it.

**2. The transitive closure** ([`closure()`](_autosummary/wads.licence_check.html.md#wads.licence_check.closure)). A copyleft distribution three
levels down is exactly as much a part of what a downstream consumer inherits as
a declared one. This is how `html2text` (GPL-3.0-or-later) sat unnoticed. The
walk reads *installed* metadata, so its answer is a fact about the environment
it runs in, not about the package – [`LicenceReport.render()`](_autosummary/wads.licence_check.html.md#wads.licence_check.LicenceReport.render) says so out
loud rather than letting the number read as universal.

**3. A demonstrated true positive** ([`self_check()`](_autosummary/wads.licence_check.html.md#wads.licence_check.self_check)). A detector with no
demonstrated true positive is a detector nobody has checked: emptying the
forbidden patterns left the originating test suite entirely green, because every
other assertion only ever checked that *nothing* matched. So every run first
proves the live policy still catches known-copyleft declarations *and* still
clears known-permissive ones, and refuses to report at all if it cannot.

This module’s own file imports nothing outside the standard library (`tomli`
only on Python 3.10, where `tomllib` does not yet exist), so it adds no
dependency beyond wads’s own core and needs no toolchain of its own. That is a
claim about the file, not isolation: importing it runs `wads/__init__`, which
pulls wads’s core dependencies into `sys.modules` like any other import.

### Module Attributes

| [`DFLT_ALLOWED`](_autosummary/wads.licence_check.html.md#wads.licence_check.DFLT_ALLOWED)             | Permissive families, matched case-insensitively against the declaration.                                                                    |
|---------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------|
| [`DFLT_COPYLEFT`](_autosummary/wads.licence_check.html.md#wads.licence_check.DFLT_COPYLEFT)            | Reciprocal (copyleft) families.                                                                                                             |
| [`DFLT_NON_COMMERCIAL`](_autosummary/wads.licence_check.html.md#wads.licence_check.DFLT_NON_COMMERCIAL)      | Non-commercial / source-available families.                                                                                                 |
| [`DFLT_FORBIDDEN`](_autosummary/wads.licence_check.html.md#wads.licence_check.DFLT_FORBIDDEN)           | What a policy forbids unless it says otherwise.                                                                                             |
| [`UNKNOWN_DECLARATIONS`](_autosummary/wads.licence_check.html.md#wads.licence_check.UNKNOWN_DECLARATIONS)     | the terms may live in a repo file no scanner reads, which is exactly where LGPL and non-commercial model weights have been found hiding.    |
| [`LADDER`](_autosummary/wads.licence_check.html.md#wads.licence_check.LADDER)                   | The metadata fields the ladder reads, in decreasing order of precision.                                                                     |
| [`COPYLEFT_CANARIES`](_autosummary/wads.licence_check.html.md#wads.licence_check.COPYLEFT_CANARIES)        | Real declarations, copied from installed `dist-info` and from the official trove classifier list, that a policy is measured against.        |
| [`CANARY_FAMILIES`](_autosummary/wads.licence_check.html.md#wads.licence_check.CANARY_FAMILIES)          | The families [`COPYLEFT_CANARIES`](_autosummary/wads.licence_check.html.md#wads.licence_check.COPYLEFT_CANARIES) covers, in declaration order.                               |
| [`PERMISSIVE_CANARIES`](_autosummary/wads.licence_check.html.md#wads.licence_check.PERMISSIVE_CANARIES)      | Real declarations that any usable policy must still clear.                                                                                  |
| [`ALL_EXTRAS`](_autosummary/wads.licence_check.html.md#wads.licence_check.ALL_EXTRAS)               | `include_extras` value meaning "every optional-dependency group".                                                                           |
| [`POLICY_TOML_PATH`](_autosummary/wads.licence_check.html.md#wads.licence_check.POLICY_TOML_PATH)         | Where the policy lives in `pyproject.toml`.                                                                                                 |
| [`CLEAN_STATUSES`](_autosummary/wads.licence_check.html.md#wads.licence_check.CLEAN_STATUSES)           | Statuses that never fail a run.                                                                                                             |
| [`DEPENDENCIES_KEY`](_autosummary/wads.licence_check.html.md#wads.licence_check.DEPENDENCIES_KEY)         | `[project]` key holding the hard requirements.                                                                                              |
| [`POLICY_TOML_KEYS`](_autosummary/wads.licence_check.html.md#wads.licence_check.POLICY_TOML_KEYS)         | TOML keys accepted in `[tool.wads.licence]`.                                                                                                |
| [`LEGACY_POLICY_KEYS`](_autosummary/wads.licence_check.html.md#wads.licence_check.LEGACY_POLICY_KEYS)       | Keys from the earlier, hand-written table shape that shipped in a couple of repos before this module existed, mapped to what to do instead. |
| [`EXCEPTION_TABLE_NAME_KEY`](_autosummary/wads.licence_check.html.md#wads.licence_check.EXCEPTION_TABLE_NAME_KEY) | Keys read off an `[[tool.wads.licence.exceptions]]` array-of-tables entry.                                                                  |
| [`THIS_ENVIRONMENT`](_autosummary/wads.licence_check.html.md#wads.licence_check.THIS_ENVIRONMENT)         | Readers bound to the interpreter running this module.                                                                                       |
| [`EXIT_OK`](_autosummary/wads.licence_check.html.md#wads.licence_check.EXIT_OK)                  | Exit codes, so the CI step and a human read the same numbers.                                                                               |
| [`EXIT_BREACH`](_autosummary/wads.licence_check.html.md#wads.licence_check.EXIT_BREACH)              | Exit codes, so the CI step and a human read the same numbers.                                                                               |
| [`EXIT_ERROR`](_autosummary/wads.licence_check.html.md#wads.licence_check.EXIT_ERROR)               | Exit codes, so the CI step and a human read the same numbers.                                                                               |

### Functions

| [`check`](_autosummary/wads.licence_check.html.md#wads.licence_check.check)([pkg_dir, policy, readers])                  | Audit a project's installed dependency closure against its policy.                                                |
|-----------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------|
| [`closure`](_autosummary/wads.licence_check.html.md#wads.licence_check.closure)(names, /, \*[, read_requires, ...])        | `names` PLUS everything they pull in, as INSTALLED in this environment.                                           |
| [`declare`](_autosummary/wads.licence_check.html.md#wads.licence_check.declare)(dist_name, /, \*[, read_metadata])         | The DECLARATION, in order of precision -- never the licence document.                                             |
| [`declared_dependencies`](_autosummary/wads.licence_check.html.md#wads.licence_check.declared_dependencies)([pkg_dir, ...])              | Every distribution the project DECLARES, read from `pyproject.toml`.                                              |
| [`declared_licence`](_autosummary/wads.licence_check.html.md#wads.licence_check.declared_licence)(dist_name, /, \*[, ...])          | The declared licence text alone -- [`declare()`](_autosummary/wads.licence_check.html.md#wads.licence_check.declare) without the source. |
| [`declared_requirements`](_autosummary/wads.licence_check.html.md#wads.licence_check.declared_requirements)([pkg_dir, ...])              | Every REQUIREMENT STRING the project declares, markers and all.                                                   |
| [`has_environment_marker`](_autosummary/wads.licence_check.html.md#wads.licence_check.has_environment_marker)(requirement, /)             | Whether a requirement is gated on an ENVIRONMENT marker, not just an extra.                                       |
| [`main`](_autosummary/wads.licence_check.html.md#wads.licence_check.main)([argv])                                       | Console-script entry point.                                                                                       |
| [`normalise`](_autosummary/wads.licence_check.html.md#wads.licence_check.normalise)(name)                                    | PEP 503 normalisation, so `ruamel.yaml` and `ruamel-yaml` are one name.                                           |
| [`read_pyproject`](_autosummary/wads.licence_check.html.md#wads.licence_check.read_pyproject)([pkg_dir])                          | Parse `pyproject.toml` from a directory (or a direct path to the file).                                           |
| [`readers_for`](_autosummary/wads.licence_check.html.md#wads.licence_check.readers_for)(search_path, /)                        | Metadata readers that see the distributions installed on `search_path`.                                           |
| [`requirement_extra`](_autosummary/wads.licence_check.html.md#wads.licence_check.requirement_extra)(requirement)                     | The extra a requirement is conditional on, or `''` if it is a hard one.                                           |
| [`requirement_name`](_autosummary/wads.licence_check.html.md#wads.licence_check.requirement_name)(requirement)                      | The distribution name out of a `Requires-Dist` / requirement string.                                              |
| [`search_path_of`](_autosummary/wads.licence_check.html.md#wads.licence_check.search_path_of)(python_executable, /)               | The `sys.path` of ANOTHER interpreter, so its packages can be read.                                               |
| [`self_check`](_autosummary/wads.licence_check.html.md#wads.licence_check.self_check)(policy, /)                              | Prove the policy still detects, and still discriminates.                                                          |
| [`verdict`](_autosummary/wads.licence_check.html.md#wads.licence_check.verdict)(name, /, \*, policy[, read_metadata, ...]) | Judge one distribution against `policy`.                                                                          |
| [`walk_closure`](_autosummary/wads.licence_check.html.md#wads.licence_check.walk_closure)(requirements, /, \*[, ...])           | The closure of `requirements` (raw requirement strings), marked up.                                               |

### Classes

| [`Canary`](_autosummary/wads.licence_check.html.md#wads.licence_check.Canary)(family, label, declaration)               | One real licence declaration a policy is measured against.                                           |
|---------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------|
| [`ClosureWalk`](_autosummary/wads.licence_check.html.md#wads.licence_check.ClosureWalk)(names, conditional)                  | The closure, plus which of its members are reachable only conditionally.                             |
| [`Declaration`](_autosummary/wads.licence_check.html.md#wads.licence_check.Declaration)(text, source)                        | What a distribution declares, and *which field* said so.                                             |
| [`LicencePolicy`](_autosummary/wads.licence_check.html.md#wads.licence_check.LicencePolicy)([allowed, forbidden, ...])         | The rule set a perimeter check is run against.                                                       |
| [`LicenceReport`](_autosummary/wads.licence_check.html.md#wads.licence_check.LicenceReport)(policy, declared, verdicts[, ...]) | What a run found: the closure it walked, and a verdict for every member.                             |
| [`MetadataReaders`](_autosummary/wads.licence_check.html.md#wads.licence_check.MetadataReaders)(read_metadata, read_requires)    | The pair of readers [`check()`](_autosummary/wads.licence_check.html.md#wads.licence_check.check) walks the closure with. |
| [`SelfCheck`](_autosummary/wads.licence_check.html.md#wads.licence_check.SelfCheck)(caught, uncovered_families)            | What a policy proved about itself before it was allowed to report.                                   |
| [`Status`](_autosummary/wads.licence_check.html.md#wads.licence_check.Status)()                                         | The verdicts a distribution can receive.                                                             |
| [`Verdict`](_autosummary/wads.licence_check.html.md#wads.licence_check.Verdict)(name, status[, declaration, note])       | One distribution's standing against the policy.                                                      |

### Exceptions

| [`DetectorError`](_autosummary/wads.licence_check.html.md#wads.licence_check.DetectorError)   | The live policy cannot detect anything, so its green result means nothing.   |
|------------------------------------------------------------------|------------------------------------------------------------------------------|

### wads.licence_check.ALL_EXTRAS *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= '\*'*

`include_extras` value meaning “every optional-dependency group”.

### wads.licence_check.CANARY_FAMILIES *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('LGPL', 'GPL', 'AGPL', 'Nethack')*

The families [`COPYLEFT_CANARIES`](_autosummary/wads.licence_check.html.md#wads.licence_check.COPYLEFT_CANARIES) covers, in declaration order.

### wads.licence_check.CLEAN_STATUSES *: [frozenset](https://docs.python.org/3/builtins/stdtypes.html#frozenset)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]* *= frozenset({'allowed', 'excepted', 'not-applicable'})*

Statuses that never fail a run. `NOT_INSTALLED` is deliberately absent:
a hard dependency the check could not read is the same confident-green
failure as reading the wrong environment. A dependency gated on an
environment marker gets `NOT_APPLICABLE` instead, which is clean.

### wads.licence_check.COPYLEFT_CANARIES *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[Canary](_autosummary/wads.licence_check.html.md#wads.licence_check.Canary), ...]* *= (('LGPL', 'argh / PyGithub (legacy LGPL classifier)', 'License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)'), ('LGPL', 'LGPLv2 classifier', 'License :: OSI Approved :: GNU Lesser General Public License v2 (LGPLv2)'), ('LGPL', 'LGPLv2+ classifier', 'License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+)'), ('LGPL', 'LGPLv3 classifier', 'License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)'), ('LGPL', 'LGPLv3+ classifier', 'License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)'), ('LGPL', 'spelled-out LGPL', 'GNU Lesser General Public License'), ('LGPL', 'bare LGPLv3+', 'LGPLv3+'), ('LGPL', 'soxr', 'LGPL-2.1-or-later'), ('GPL', 'GPLv3 classifier', 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)'), ('GPL', 'GPLv2+ classifier', 'License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)'), ('GPL', 'spelled-out GPL', 'GNU General Public License v2 or later'), ('GPL', 'html2text', 'GPL-3.0-or-later'), ('AGPL', 'AGPLv3+ classifier', 'License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)'), ('AGPL', 'AGPLv3 classifier', 'License :: OSI Approved :: GNU Affero General Public License v3'), ('AGPL', 'spelled-out AGPL', 'GNU Affero General Public License v3'), ('AGPL', 'bare AGPLv3', 'AGPLv3'), ('AGPL', 'ultralytics', 'AGPL-3.0'), ('Nethack', 'Nethack GPL classifier', 'License :: OSI Approved :: Nethack General Public License'))*

Real declarations, copied from installed `dist-info` and from the official
trove classifier list, that a policy is measured against. See
[`self_check()`](_autosummary/wads.licence_check.html.md#wads.licence_check.self_check), which requires each FAMILY to be caught whole or not at
all.

The list is deliberately wider than the acronyms: it was hand-picked from the
same mental model as the patterns once, and the four LGPL spellings that
escaped were exactly the ones nobody thought to write down. Every officially
published GPL-family trove classifier spelling is here, plus the bare
acronym-with-version and the fully-spelled-out prose forms.

### *class* wads.licence_check.Canary(family, label, declaration)

Bases: [`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)

One real licence declaration a policy is measured against.

`family` is what makes the measurement useful. Permitting a whole family
is a coherent stance somebody can take on purpose – LGPL for dynamically
linked libraries is the usual one. Catching *part* of a family is never a
stance: it means the author meant to catch it and the spelling got away.
[`self_check()`](_autosummary/wads.licence_check.html.md#wads.licence_check.self_check) allows the first and refuses the second.

#### declaration *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

Alias for field number 2

#### family *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

Alias for field number 0

#### label *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

Alias for field number 1

### *class* wads.licence_check.ClosureWalk(names, conditional)

Bases: [`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)

The closure, plus which of its members are reachable only conditionally.

`conditional` is every name that could ONLY be reached by following a
requirement gated on an environment marker (`python_version`,
`sys_platform`, …). Those are the names whose absence from the
environment being read is expected rather than alarming; every other absence
means the check did not get to look at something the project genuinely
requires.

#### conditional *: [frozenset](https://docs.python.org/3/builtins/stdtypes.html#frozenset)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]*

Alias for field number 1

#### names *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]*

Alias for field number 0

### wads.licence_check.DEPENDENCIES_KEY *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'dependencies'*

`[project]` key holding the hard requirements. Named because two different
absences of it mean two different things – see [`declared_requirements()`](_autosummary/wads.licence_check.html.md#wads.licence_check.declared_requirements).

### wads.licence_check.DFLT_ALLOWED *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('\\\\bMIT\\\\b', '\\\\bBSD\\\\b', '\\\\b0BSD\\\\b', '\\\\bApache[- ]?2', '\\\\bApache Software License\\\\b', '\\\\bISC\\\\b', '\\\\bPython Software Foundation\\\\b', '\\\\bPSF\\\\b', '\\\\bHPND\\\\b', '\\\\bUnlicense\\\\b', '\\\\bCC0\\\\b', '\\\\bZlib\\\\b', '\\\\bBoost Software License\\\\b', '\\\\bBSL[- ]?1\\\\.0\\\\b')*

Permissive families, matched case-insensitively against the declaration.
`\b0BSD\b` is its own entry because `\bBSD\b` cannot reach inside
`0BSD` – there is no word boundary between a digit and a letter – so the
SPDX id of one of the most permissive licences in existence would otherwise
land in UNCLASSIFIED.

### wads.licence_check.DFLT_COPYLEFT *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('\\\\bAGPL', '\\\\bAffero\\\\b', '\\\\bGPL(?![\\\\w.+-]\*\\\\s+with\\\\b)', '\\\\bGNU General Public\\\\b', '\\\\bLGPL', '\\\\bLesser General Public\\\\b', '\\\\bLibrary General Public\\\\b', '\\\\bNethack General Public\\\\b', '\\\\bEUPL\\\\b')*

Reciprocal (copyleft) families.

Every pattern here is anchored at its LEFT edge only, and that asymmetry is
the whole point. `\bGPL` cannot reach inside `LGPL` (there is no word
boundary between `L` and `G`), so LGPL and AGPL need their own entries –
but a TRAILING `\b` on those entries is a hole, not a safeguard: it cannot
match `LGPLv3`, `LGPLv2+` or `AGPLv3`, which is every modern trove
classifier and most SPDX-adjacent free text. A gate built to catch copyleft
spent its first life letting the four commonest LGPL spellings straight
through. [`COPYLEFT_CANARIES`](_autosummary/wads.licence_check.html.md#wads.licence_check.COPYLEFT_CANARIES) now pins all of them.

Every family carries a spelled-out pattern beside its acronym, because the
prose forms (“GNU General Public License v2 or later”) carry no acronym at
all. They are deliberately family-SPECIFIC rather than one broad `General
Public License`: a policy that permits one family and gates on the others is
a coherent stance, and a cross-family pattern makes it inexpressible –
[`self_check()`](_autosummary/wads.licence_check.html.md#wads.licence_check.self_check) would see a family caught in part and refuse to run.

The lookahead on `\bGPL` spares the SPDX `WITH <exception>` spellings
(`GPL-2.0-only WITH Classpath-exception-2.0`, `GPLv2 with linking
exception`) and nothing subtler; anything subtler belongs in `exceptions`,
with a written reason, rather than in a regex nobody will re-derive.

### wads.licence_check.DFLT_FORBIDDEN *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('\\\\bAGPL', '\\\\bAffero\\\\b', '\\\\bGPL(?![\\\\w.+-]\*\\\\s+with\\\\b)', '\\\\bGNU General Public\\\\b', '\\\\bLGPL', '\\\\bLesser General Public\\\\b', '\\\\bLibrary General Public\\\\b', '\\\\bNethack General Public\\\\b', '\\\\bEUPL\\\\b', '\\\\bBusiness Source\\\\b', '\\\\bBUSL\\\\b', '\\\\bSSPL\\\\b', '\\\\bElastic[- ]?(2\\\\.0|License|v2)\\\\b', '(?:\\\\b|-)(?:open)?rail(?:-m)?\\\\b', '\\\\bCC[- ]BY[- ]NC\\\\b', '\\\\bNon[- ]?Commercial\\\\b', '\\\\bProprietary\\\\b')*

What a policy forbids unless it says otherwise.

### wads.licence_check.DFLT_NON_COMMERCIAL *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('\\\\bBusiness Source\\\\b', '\\\\bBUSL\\\\b', '\\\\bSSPL\\\\b', '\\\\bElastic[- ]?(2\\\\.0|License|v2)\\\\b', '(?:\\\\b|-)(?:open)?rail(?:-m)?\\\\b', '\\\\bCC[- ]BY[- ]NC\\\\b', '\\\\bNon[- ]?Commercial\\\\b', '\\\\bProprietary\\\\b')*

Non-commercial / source-available families. These are not copyleft, but they
restrict redistribution or hosting, and a classifier-only gate cannot see
them: Arize Phoenix ships Elastic-2.0 in its `License` field with no trove
classifier at all.

### *class* wads.licence_check.Declaration(text, source)

Bases: [`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)

What a distribution declares, and *which field* said so.

Carrying the source is not decoration: it is the difference between “argh is
LGPL” and “argh is LGPL according to its trove classifier”, and only the
second is a claim a reader can go and check.

#### *property* is_blank *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

True when the distribution declared nothing usable.

#### source *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

Alias for field number 1

#### text *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

Alias for field number 0

### *exception* wads.licence_check.DetectorError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

The live policy cannot detect anything, so its green result means nothing.

### wads.licence_check.EXCEPTION_TABLE_NAME_KEY *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)* *= 'dependency'*

Keys read off an `[[tool.wads.licence.exceptions]]` array-of-tables entry.
`dependency` and `reason` are required; the rest are adjudication
provenance and are folded into the rendered reason so a reader sees when the
call was made and where to go and argue with it.

### wads.licence_check.EXIT_BREACH *= 1*

Exit codes, so the CI step and a human read the same numbers.

### wads.licence_check.EXIT_ERROR *= 2*

Exit codes, so the CI step and a human read the same numbers.

### wads.licence_check.EXIT_OK *= 0*

Exit codes, so the CI step and a human read the same numbers.

### wads.licence_check.LADDER *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('License-Expression', 'Classifier', 'License')*

The metadata fields the ladder reads, in decreasing order of precision.

### wads.licence_check.LEGACY_POLICY_KEYS *: [Mapping](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [str](https://docs.python.org/3/builtins/stdtypes.html#str)]* *= mappingproxy({'allow': 'rename it to \`allowed\` (it pairs with \`forbidden\`, where \`allow\` had no counterpart)', 'deny': 'rename it to \`forbidden\`', 'project_licence': "drop it -- the project's own licence is \`[project].license\`, and restating it here is a second source of truth that nothing reads", 'project-licence': "drop it -- the project's own licence is \`[project].license\`, and restating it here is a second source of truth that nothing reads"})*

Keys from the earlier, hand-written table shape that shipped in a couple of
repos before this module existed, mapped to what to do instead.

Two spellings of the same table is worse than either one: a repo written
against the other shape reads as configured and is not. These are rejected
rather than silently aliased – but rejected with the migration, not with a
bare “unknown key”, because the person hitting this did not choose it.

### *class* wads.licence_check.LicencePolicy(allowed=('\\\\\\\\bMIT\\\\\\\\b', '\\\\\\\\bBSD\\\\\\\\b', '\\\\\\\\b0BSD\\\\\\\\b', '\\\\\\\\bApache[- ]?2', '\\\\\\\\bApache Software License\\\\\\\\b', '\\\\\\\\bISC\\\\\\\\b', '\\\\\\\\bPython Software Foundation\\\\\\\\b', '\\\\\\\\bPSF\\\\\\\\b', '\\\\\\\\bHPND\\\\\\\\b', '\\\\\\\\bUnlicense\\\\\\\\b', '\\\\\\\\bCC0\\\\\\\\b', '\\\\\\\\bZlib\\\\\\\\b', '\\\\\\\\bBoost Software License\\\\\\\\b', '\\\\\\\\bBSL[- ]?1\\\\\\\\.0\\\\\\\\b'), forbidden=('\\\\\\\\bAGPL', '\\\\\\\\bAffero\\\\\\\\b', '\\\\\\\\bGPL(?![\\\\\\\\w.+-]\*\\\\\\\\s+with\\\\\\\\b)', '\\\\\\\\bGNU General Public\\\\\\\\b', '\\\\\\\\bLGPL', '\\\\\\\\bLesser General Public\\\\\\\\b', '\\\\\\\\bLibrary General Public\\\\\\\\b', '\\\\\\\\bNethack General Public\\\\\\\\b', '\\\\\\\\bEUPL\\\\\\\\b', '\\\\\\\\bBusiness Source\\\\\\\\b', '\\\\\\\\bBUSL\\\\\\\\b', '\\\\\\\\bSSPL\\\\\\\\b', '\\\\\\\\bElastic[- ]?(2\\\\\\\\.0|License|v2)\\\\\\\\b', '(?: \\\\\\\\b|-)(?:open)?rail(?:-m)?\\\\\\\\b', '\\\\\\\\bCC[- ]BY[- ]NC\\\\\\\\b', '\\\\\\\\bNon[- ]?Commercial\\\\\\\\b', '\\\\\\\\bProprietary\\\\\\\\b'), exceptions=<factory>, include_extras=(), unknown_is_failure=True, unclassified_is_failure=False)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

The rule set a perimeter check is run against.

`exceptions` maps a distribution name to the WRITTEN reason it is
tolerated. Listing beats silence: a name here is a decision somebody made
and can be asked about, where a name quietly missing from `forbidden` is
not.

#### exception_for(name,)

The recorded reason `name` is tolerated, or `''`.

Matched on the PEP 503 normalised name, so a policy written with
`typing_extensions` still clears the distribution installed metadata
calls `typing-extensions`. Getting this wrong is invisible: the
exception simply never applies and the report keeps flagging a name
somebody believes they already adjudicated.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> LicencePolicy(exceptions={'Ruamel.YAML': 'audited'}).exception_for(
...     'ruamel-yaml')
'audited'
```

#### *classmethod* from_mapping(config,)

Build a policy from a `[tool.wads.licence]` table.

Accepts both TOML-style `include-extras` and Python-style
`include_extras`. An unrecognised key is an error rather than a
silently ignored typo – a misspelt `forbiden` would leave the
perimeter on its defaults while reading as configured.

Value TYPES are checked too, and for the same reason the key names are:
`allowed = "MIT"` used to become the three regexes `M`, `I`, `T`
without a word, and an `exceptions` array-of-tables used to collapse
into `{'dependency': 'reason'}`. Both read as configured. Neither was.

* **Return type:**
  [`LicencePolicy`](_autosummary/wads.licence_check.html.md#wads.licence_check.LicencePolicy)

```pycon
>>> policy = LicencePolicy.from_mapping(
...     {'allowed': ['MIT'], 'include-extras': ['*'], 'unknown-is-failure': False}
... )
>>> policy.allowed, policy.include_extras, policy.unknown_is_failure
(('MIT',), ('*',), False)
>>> policy.forbidden == DFLT_FORBIDDEN  # untouched keys keep their defaults
True
```

`exceptions` takes either the terse map or the array-of-tables that
carries an adjudication record (see `_exceptions_mapping()`):

```pycon
>>> LicencePolicy.from_mapping({'exceptions': [
...     {'dependency': 'PyGithub', 'reason': 'Accepted.', 'decided': '2026-08-30'}
... ]}).exception_for('pygithub')
'Accepted. [decided: 2026-08-30]'
```

```pycon
>>> try:
...     LicencePolicy.from_mapping({'forbiden': ['GPL']})
... except ValueError as error:
...     print(error)
unknown [tool.wads.licence] key 'forbiden'; known keys are: allowed, enabled,
exceptions, forbidden, include-extras, unclassified-is-failure, unknown-is-failure
```

A key from the earlier hand-written table shape is rejected with the
migration rather than with a bare “unknown key”:

```pycon
>>> try:
...     LicencePolicy.from_mapping({'allow': ['MIT']})
... except ValueError as error:
...     print(error)
[tool.wads.licence] key 'allow' is from the earlier table shape: rename it to
`allowed` (it pairs with `forbidden`, where `allow` had no counterpart)
```

#### *classmethod* from_pyproject(pkg_dir='.', , , pyproject=None)

The policy declared in `[tool.wads.licence]`, or the defaults.

* **Return type:**
  [`LicencePolicy`](_autosummary/wads.licence_check.html.md#wads.licence_check.LicencePolicy)

#### matched_allowed(declaration,)

The first permissive pattern `declaration` matches, or `''`.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

#### matched_forbidden(declaration,)

The first forbidden pattern `declaration` matches, or `''`.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

#### permissive_option(declaration,)

A disjunct of `declaration` that is permissive and not forbidden.

SPDX’s `OR` is a genuine choice offered to the recipient, so a
declaration is only as restrictive as its most permissive option. Reading
the whole string as one blob gets that backwards – `marisa-trie`
declares `MIT AND (BSD-2-Clause OR LGPL-2.1-or-later)` and was failed on
the option nobody has to take:

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> LicencePolicy().permissive_option('MIT AND (BSD-2-Clause OR LGPL-2.1-or-later)')
'MIT AND (BSD-2-Clause'
```

When every option is restricted there is nothing to pick, and the
declaration stands as it is:

```pycon
>>> LicencePolicy().permissive_option('LGPL-2.1-only OR MPL-1.1')
''
```

### *class* wads.licence_check.LicenceReport(policy, declared, verdicts, canaries=(), uncovered_families=(), pkg_dir='.')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

What a run found: the closure it walked, and a verdict for every member.

#### as_dict()

A JSON-serialisable view, for fleet-wide sweeps.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

#### *property* closure *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]*

Every distribution the walk reached.

#### *property* failures *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[Verdict](_autosummary/wads.licence_check.html.md#wads.licence_check.Verdict), ...]*

The verdicts that make this run fail, under this policy.

#### of_status(\*statuses)

Every verdict whose status is one of `statuses`.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`Verdict`](_autosummary/wads.licence_check.html.md#wads.licence_check.Verdict), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

#### *property* ok *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

Whether the perimeter holds.

#### render()

A human-readable report, failures first.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

#### *property* stale_exceptions *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]*

Recorded exceptions for distributions the walk never reached.

An exception for a distribution that has left the tree is stale advice,
and one whose licence has since changed is worse: it reads as
adjudicated when nobody has looked at the current terms. Reported, not
failed – an extra is often the reason, and breaking a build over a
tidiness issue is how gates get switched off.

### *class* wads.licence_check.MetadataReaders(read_metadata, read_requires)

Bases: [`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)

The pair of readers [`check()`](_autosummary/wads.licence_check.html.md#wads.licence_check.check) walks the closure with.

#### read_metadata *: [Callable](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[str](https://docs.python.org/3/builtins/stdtypes.html#str)], [Any](https://docs.python.org/3/library/typing.html#typing.Any)]*

Alias for field number 0

#### read_requires *: [Callable](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[str](https://docs.python.org/3/builtins/stdtypes.html#str)], [list](https://docs.python.org/3/builtins/stdtypes.html#list) | [None](https://docs.python.org/3/builtins/constants.html#None)]*

Alias for field number 1

### wads.licence_check.PERMISSIVE_CANARIES *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('MIT', 'BSD-3-Clause', '0BSD', 'Apache-2.0', 'Apache Software License', 'ISC', 'BSL-1.0', 'License :: OSI Approved :: Boost Software License 1.0 (BSL-1.0)')*

Real declarations that any usable policy must still clear. A policy that
flags these is not strict, it is broken, and it would fail every repo.
`BSL-1.0` is the Boost Software License, permissive and OSI-approved –
it is here because a `\bBSL\b` pattern aimed at the Business Source
License (`BUSL-1.1`) flags it, and that pattern shipped once already.

### wads.licence_check.POLICY_TOML_KEYS *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('enabled', 'allowed', 'forbidden', 'exceptions', 'include-extras', 'unknown-is-failure', 'unclassified-is-failure')*

TOML keys accepted in `[tool.wads.licence]`. `enabled` is read by
[`wads.ci_config`](_autosummary/wads.ci_config.html.md#module-wads.ci_config) for the CI gate rather than by the policy itself.

### wads.licence_check.POLICY_TOML_PATH *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ('tool', 'wads', 'licence')*

Where the policy lives in `pyproject.toml`.

### *class* wads.licence_check.SelfCheck(caught, uncovered_families)

Bases: [`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)

What a policy proved about itself before it was allowed to report.

#### caught *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]*

Alias for field number 0

#### uncovered_families *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]*

Alias for field number 1

### *class* wads.licence_check.Status

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

The verdicts a distribution can receive. Not an Enum: these are printed.

### wads.licence_check.THIS_ENVIRONMENT *= (<function metadata>, <function requires>)*

Readers bound to the interpreter running this module.

### wads.licence_check.UNKNOWN_DECLARATIONS *: [frozenset](https://docs.python.org/3/builtins/stdtypes.html#frozenset)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]* *= frozenset({'', 'NOASSERTION', 'NONE', 'UNKNOWN'})*

the
terms may live in a repo file no scanner reads, which is exactly where LGPL
and non-commercial model weights have been found hiding.

* **Type:**
  Declarations that say nothing. Blank is not “fine”, it is *unaudited*

### *class* wads.licence_check.Verdict(name, status, declaration=('', ''), note='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One distribution’s standing against the policy.

#### *property* is_clean *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

Whether this verdict is one that never fails a run.

### wads.licence_check.check(pkg_dir='.', /, \*, policy=None, readers=(<function metadata>, <function requires>))

Audit a project’s installed dependency closure against its policy.

`policy=None` loads `[tool.wads.licence]` from the project’s
`pyproject.toml`, falling back to the module defaults.

Raises [`DetectorError`](_autosummary/wads.licence_check.html.md#wads.licence_check.DetectorError) before reading anything if the live policy
could not detect every known-copyleft canary declaration, if the project does
not declare its dependencies statically, and again if not one declared
dependency is installed – all states in which a green result would be a lie
rather than a finding.

A dependency that IS declared and is NOT installed here is a failure, not a
footnote: it is a piece of the perimeter nobody looked at, and the previous
all-or-nothing refusal let one installed survivor turn three unread copyleft
dependencies into a green. The one honest exception is a requirement gated on
an environment marker (`tomli; python_version < "3.11"`), which gets
`NOT_APPLICABLE` and is reported without failing.

* **Return type:**
  [`LicenceReport`](_autosummary/wads.licence_check.html.md#wads.licence_check.LicenceReport)

### wads.licence_check.closure(names, /, \*, read_requires=<function requires>, include_extras=())

`names` PLUS everything they pull in, as INSTALLED in this environment.

Checking only the declared names is a smaller claim than it reads as:
`pip install <pkg>` installs their transitive closure, and a copyleft
distribution three levels down is exactly as much a part of what a
downstream consumer inherits.

By default the walk skips requirements gated on `extra == "..."`, because
a bare install does not take them. `include_extras=('*',)` takes all of
them; a tuple of names takes those.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

```pycon
>>> tree = {
...     'citeget': ['html2text', 'requests', 'pytest; extra == "test"'],
...     'html2text': [],
...     'requests': ['certifi'],
...     'certifi': [],
... }
>>> closure(['citeget'], read_requires=tree.get)
('certifi', 'citeget', 'html2text', 'requests')
>>> closure(['citeget'], read_requires=tree.get, include_extras=('test',))
('certifi', 'citeget', 'html2text', 'pytest', 'requests')
```

An uninstalled name simply ends that branch – it is still reported by
[`check()`](_autosummary/wads.licence_check.html.md#wads.licence_check.check), but it cannot contribute requirements it does not have:

```pycon
>>> closure(['citeget', 'nowhere'], read_requires=tree.get)
('certifi', 'citeget', 'html2text', 'nowhere', 'requests')
```

[`walk_closure()`](_autosummary/wads.licence_check.html.md#wads.licence_check.walk_closure) is the same walk, and additionally says which members
are reachable only through an environment-marker-gated requirement.

### wads.licence_check.declare(dist_name, /, \*, read_metadata=<function metadata>)

The DECLARATION, in order of precision – never the licence document.

`read_metadata` is the seam onto the metadata source; it must return
something with `.get` / `.get_all`, which is what both
[`importlib.metadata.metadata()`](https://docs.python.org/3/library/importlib.metadata.html#importlib.metadata.metadata) and a parsed `METADATA` email message
give you.

* **Return type:**
  [`Declaration`](_autosummary/wads.licence_check.html.md#wads.licence_check.Declaration)

```pycon
>>> from email import message_from_string
>>> def fake(name, records=None):
...     return message_from_string(records[name])
>>> import functools
>>> records = {
...     'click': 'License-Expression: BSD-3-Clause\n',
...     'argh': ('Classifier: License :: OSI Approved :: '
...              'GNU Library or Lesser General Public License (LGPL)\n'),
...     'i2': 'License: Apache Software License\n',
... }
>>> read = functools.partial(fake, records=records)
>>> declare('click', read_metadata=read)
Declaration(text='BSD-3-Clause', source='License-Expression')
>>> declare('i2', read_metadata=read)
Declaration(text='Apache Software License', source='License')
>>> declare('argh', read_metadata=read).source
'Classifier'
```

The free-text field is read ONE LINE deep. numpy’s field is the whole
48k-character licence document, and it contains an LGPL URL for a vendored
component’s notice – substring-scanning it reports BSD-3-Clause numpy as
copyleft:

```pycon
>>> numpy_field = 'Copyright (c) 2005-2024, NumPy Developers.\n  http://x/lgpl'
>>> read_numpy = functools.partial(fake, records={'numpy': f'License: {numpy_field}'})
>>> declare('numpy', read_metadata=read_numpy).text
'Copyright (c) 2005-2024, NumPy Developers.'
```

### wads.licence_check.declared_dependencies(pkg_dir='.', , , include_extras=(), pyproject=None)

Every distribution the project DECLARES, read from `pyproject.toml`.

Derived, never restated: a dependency added to `pyproject.toml` and
forgotten in a hand-written list is a dependency the perimeter never looks
at, and the perimeter reads as green either way.

[`declared_requirements()`](_autosummary/wads.licence_check.html.md#wads.licence_check.declared_requirements) without the version specifiers and markers.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

```pycon
>>> declared_dependencies(pyproject={'project': {
...     'dependencies': ['html2text', 'requests>=2'],
...     'optional-dependencies': {'test': ['pytest']},
... }})
('html2text', 'requests')
>>> declared_dependencies(include_extras=('test',), pyproject={'project': {
...     'dependencies': ['html2text'],
...     'optional-dependencies': {'test': ['pytest']},
... }})
('html2text', 'pytest')
```

### wads.licence_check.declared_licence(dist_name, /, \*, read_metadata=<function metadata>)

The declared licence text alone – [`declare()`](_autosummary/wads.licence_check.html.md#wads.licence_check.declare) without the source.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### wads.licence_check.declared_requirements(pkg_dir='.', , , include_extras=(), pyproject=None)

Every REQUIREMENT STRING the project declares, markers and all.

Requirement strings rather than bare names, because the marker is the
information: `tomli; python_version < "3.11"` being absent from a 3.12
environment is expected, and `html2text` being absent is the check not
having looked.

Parsed with a real TOML parser rather than scanned as text. That is not a
style preference: a text scan that ends the `dependencies` array at the
first `]` stops at the first requirement carrying an extra
(`"uvicorn[standard]"`) and silently drops every name below it – one line
disarming the whole check. A parser cannot have that bug.

An ABSENT `dependencies` key, or one listed in `dynamic`, is refused
rather than read as zero. `dependencies = []` is a project saying it has
none; a missing key is a project whose requirements live somewhere this
function cannot see (`setup.py`, `requirements.txt`), and reporting
“declared: 0 … PERIMETER HOLDS” over it is the confident-green failure this
module exists to stop.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

```pycon
>>> declared_requirements(pyproject={'project': {
...     'dependencies': ['html2text', 'requests>=2'],
...     'optional-dependencies': {'test': ['pytest']},
... }})
('html2text', 'requests>=2')
>>> try:
...     declared_requirements(pyproject={'project': {
...         'name': 'x', 'dynamic': ['dependencies']}})
... except DetectorError as error:
...     print(str(error)[:58])
this project lists `dependencies` in [project].dynamic, so
```

### wads.licence_check.has_environment_marker(requirement,)

Whether a requirement is gated on an ENVIRONMENT marker, not just an extra.

This is the one honest reason a declared dependency can be missing from the
environment being read. The gate runs on one interpreter, on one OS, so a
dependency gated on `python_version < "3.11"` or `sys_platform ==
"win32"` is structurally invisible – and an absent-but-required
distribution is the same confident-green failure as reading the wrong
environment altogether. Telling the two apart is what lets one be a note and
the other a failure.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

```pycon
>>> has_environment_marker('tomli>=1.0.0; python_version < "3.11"')
True
>>> has_environment_marker('pytest; extra == "test"')
False
>>> has_environment_marker('requests>=2')
False
```

### wads.licence_check.main(argv=None)

Console-script entry point. Returns the process exit code.

* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int)

### wads.licence_check.normalise(name)

PEP 503 normalisation, so `ruamel.yaml` and `ruamel-yaml` are one name.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> normalise('Ruamel.YAML')
'ruamel-yaml'
```

### wads.licence_check.read_pyproject(pkg_dir='.')

Parse `pyproject.toml` from a directory (or a direct path to the file).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### wads.licence_check.readers_for(search_path,)

Metadata readers that see the distributions installed on `search_path`.

* **Return type:**
  [`MetadataReaders`](_autosummary/wads.licence_check.html.md#wads.licence_check.MetadataReaders)

```pycon
>>> import sys
>>> readers = readers_for(sys.path)
>>> readers.read_metadata('wads')['Name']
'wads'
```

### wads.licence_check.requirement_extra(requirement)

The extra a requirement is conditional on, or `''` if it is a hard one.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> requirement_extra('anthropic; extra == "vision"')
'vision'
>>> requirement_extra('pydantic>=2')
''
```

### wads.licence_check.requirement_name(requirement)

The distribution name out of a `Requires-Dist` / requirement string.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> requirement_name('uvicorn[standard]>=0.20; python_version >= "3.9"')
'uvicorn'
```

### wads.licence_check.search_path_of(python_executable,)

The `sys.path` of ANOTHER interpreter, so its packages can be read.

This is what lets the CI gate run the tool from an isolated `uvx`
environment while auditing the project’s own `.venv`. Without it the tool
would read *its own* dependencies and report a confident, wrong green.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

### wads.licence_check.self_check(policy,)

Prove the policy still detects, and still discriminates. Or refuse to run.

Three failure modes, all observed. A policy whose `forbidden` list has been
emptied reports every repo clean and nobody notices, because every other
assertion in a licence check only ever says that *nothing* matched. A policy
so broad it flags `MIT` fails every repo, which gets the gate switched off
within the day. And – the one that actually shipped – a policy that catches
*some* spellings of a family while missing the commonest ones reads as a pass
on the strength of the spellings it does catch.

So the bar is per FAMILY, caught whole or not at all. Permitting a family
outright is a stance somebody can take on purpose and defend; catching four
of a family’s nine spellings is never a stance, it is the bug. A family
nobody covers is reported rather than assumed – see
`LicenceReport.uncovered_families`, which puts it in every run’s output
so “we do not gate on GPL here” is a visible fact rather than an inference.

* **Return type:**
  [`SelfCheck`](_autosummary/wads.licence_check.html.md#wads.licence_check.SelfCheck)

```pycon
>>> checked = self_check(LicencePolicy())
>>> len(checked.caught) == len(COPYLEFT_CANARIES), checked.uncovered_families
(True, ())
```

Dropping a whole family is allowed, and is then stated out loud:

```pycon
>>> lgpl_ok = LicencePolicy(forbidden=tuple(
...     p for p in DFLT_FORBIDDEN if 'LGPL' not in p and 'Lesser' not in p))
>>> self_check(lgpl_ok).uncovered_families
('LGPL',)
```

Catching only PART of a family is not. This is the shape of the real bug:
`\bLGPL\b` catches the legacy classifier and misses `LGPLv3`:

```pycon
>>> try:
...     self_check(LicencePolicy(forbidden=(r'\bAGPL', r'\bGNU Affero\b',
...                                        r'\bLGPL\b')))
... except DetectorError as error:
...     print('LGPLv3 classifier' in str(error))
True
```

A policy that detects nothing refuses to report:

```pycon
>>> try:
...     self_check(LicencePolicy(forbidden=()))
... except DetectorError as error:
...     print(str(error)[:52])
this policy's `forbidden` patterns catch none of the
```

And so does one broad enough to flag everything:

```pycon
>>> try:
...     self_check(LicencePolicy(forbidden=(r'\bAGPL', r'\bApache\b')))
... except DetectorError as error:
...     print('over-broad, and it names what it wrongly flagged:',
...           'Apache-2.0' in str(error))
over-broad, and it names what it wrongly flagged: True
```

### wads.licence_check.verdict(name, /, \*, policy, read_metadata=<function metadata>, conditional=False)

Judge one distribution against `policy`.

An adjudicated exception wins over everything, including a forbidden match:
that is what recording one *means*, and the reason travels with it.

* **Return type:**
  [`Verdict`](_autosummary/wads.licence_check.html.md#wads.licence_check.Verdict)

```pycon
>>> from email import message_from_string
>>> import functools
>>> records = {'html2text': 'License-Expression: GPL-3.0-or-later\n',
...            'requests': 'License-Expression: Apache-2.0\n',
...            'certifi': 'License-Expression: MPL-2.0\n',
...            'mystery': 'Name: mystery\n'}
>>> read = functools.partial(lambda n, r: message_from_string(r[n]), r=records)
>>> verdict('html2text', policy=LicencePolicy(), read_metadata=read).status
'forbidden'
>>> verdict('requests', policy=LicencePolicy(), read_metadata=read).status
'allowed'
```

MPL-2.0 is deliberately neither: it is file-level weak copyleft, so it is
not forbidden, and it matches no permissive family either. It surfaces as
`unclassified` – a decision to make, not a build to break.

```pycon
>>> verdict('certifi', policy=LicencePolicy(), read_metadata=read).status
'unclassified'
>>> verdict('mystery', policy=LicencePolicy(), read_metadata=read).status
'unknown'
```

A declaration matching a forbidden AND a permissive pattern is not simply
forbidden. Where SPDX’s `OR` says the recipient picks, the permissive pick
is taken; where the two spellings merely sit side by side – which is what
joining a distribution’s `License ::` classifiers produces – nobody knows
which applies to which file, so it becomes a question rather than a failure:

```pycon
>>> both = {'marisa-trie': ('License-Expression: '
...                         'MIT AND (BSD-2-Clause OR LGPL-2.1-or-later)\n'),
...         'docutils': ('Classifier: License :: Public Domain\n'
...                      'Classifier: License :: OSI Approved :: BSD License\n'
...                      'Classifier: License :: OSI Approved :: '
...                      'GNU General Public License (GPL)\n')}
>>> read_both = functools.partial(lambda n, r: message_from_string(r[n]), r=both)
>>> verdict('marisa-trie', policy=LicencePolicy(), read_metadata=read_both).status
'allowed'
>>> verdict('docutils', policy=LicencePolicy(), read_metadata=read_both).status
'unclassified'
```

### wads.licence_check.walk_closure(requirements, /, \*, read_requires=<function requires>, include_extras=())

The closure of `requirements` (raw requirement strings), marked up.

* **Return type:**
  [`ClosureWalk`](_autosummary/wads.licence_check.html.md#wads.licence_check.ClosureWalk)

```pycon
>>> tree = {'app': ['requests', 'tomli; python_version < "3.11"'],
...         'requests': ['certifi'], 'certifi': [], 'tomli': []}
>>> walk = walk_closure(['app'], read_requires=tree.get)
>>> walk.names
('app', 'certifi', 'requests', 'tomli')
>>> sorted(walk.conditional)
['tomli']
```


# _autosummary/wads.licensing.html.md

# wads.licensing

Licensing

### Functions

| `get_licenses`([refresh])                                                                         |                                                                                                                             |
|---------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------|
| [`get_licenses_from_github`](_autosummary/wads.licensing.html.md#wads.licensing.get_licenses_from_github)()                       | get_licenses_json_from_github You need to have a github token placed in the right place for this! See pygithub for details. |
| `license_body`([license, ...])                                                                    |                                                                                                                             |
| `license_info`([license, ...])                                                                    |                                                                                                                             |
| `licenses_dict`([refresh])                                                                        |                                                                                                                             |
| [`resolve_author`](_autosummary/wads.licensing.html.md#wads.licensing.resolve_author)([author, pyproject_authors, url]) | Resolve the author name following a priority chain.                                                                         |
| [`substitute_license_placeholders`](_autosummary/wads.licensing.html.md#wads.licensing.substitute_license_placeholders)(license_text)    | Replace placeholders in license text with actual values.                                                                    |

### wads.licensing.get_licenses_from_github()

get_licenses_json_from_github
You need to have a github token placed in the right place for this!
See pygithub for details.

```text
license_jsons = get_licenses_json_from_github()
```

### wads.licensing.resolve_author(author=None, pyproject_authors=None, url=None)

Resolve the author name following a priority chain.

Priority order:

1. If author is explicitly provided, use it
2. If pyproject_authors is provided, use the first author’s name
3. If url is a GitHub URL, extract the org/user from it
4. If WADS_DFLT_AUTHOR env variable is set, use it
5. Return placeholder if nothing else works

* **Parameters:**
  * **author** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – Explicitly provided author name
  * **pyproject_authors** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`list`](https://docs.python.org/3/builtins/stdtypes.html#list)]) – List of author dicts from pyproject.toml
  * **url** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – Project URL (e.g., GitHub URL)
* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)
* **Returns:**
  Resolved author name or placeholder

```pycon
>>> resolve_author(author='John Doe')
'John Doe'
>>> resolve_author(pyproject_authors=[{'name': 'Jane Smith'}])
'Jane Smith'
>>> resolve_author(url='https://github.com/myorg/myrepo')
'myorg'
```

### wads.licensing.substitute_license_placeholders(license_text, author=None, year=None)

Replace placeholders in license text with actual values.

Common placeholders:

- [yyyy], [year] -> current year
- [fullname], [name of copyright owner] -> author name
- 1. -> © (optional enhancement)

* **Parameters:**
  * **license_text** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – License text with placeholders
  * **author** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – Author/copyright owner name (if None, placeholder remains)
  * **year** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`int`](https://docs.python.org/3/builtins/functions.html#int)]) – Year for copyright (if None, uses current year)
* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)
* **Returns:**
  License text with substituted values

```pycon
>>> text = "Copyright [yyyy] [fullname]"
>>> result = substitute_license_placeholders(text, author='John', year=2025)
>>> result
'Copyright 2025 John'
```


# _autosummary/wads.migration.html.md

# wads.migration

Migration tools for converting old setuptools/CI configurations to modern formats.

This module provides functions to migrate:

- setup.cfg files to pyproject.toml (hatching format)
- Old GitHub CI workflows to modern 2025 format

Key Functions:
: migrate_setuptools_to_hatching: Convert setup.cfg to pyproject.toml
  migrate_github_ci_old_to_new: Convert old CI scripts to new format

### Example

```pycon
>>> from wads.migration import migrate_setuptools_to_hatching
>>> # From a file (use actual file path)
>>> pyproject = migrate_setuptools_to_hatching('setup.cfg')
>>>
>>> # From a dict with complete metadata
>>> cfg = {
...     'metadata': {
...         'name': 'myproject',
...         'version': '0.1.0',
...         'description': 'A sample project',
...         'url': 'https://github.com/user/myproject',
...         'license': 'MIT'
...     }
... }
>>> result = migrate_setuptools_to_hatching(cfg)
>>> 'name = "myproject"' in result
True
```

### Functions

| [`analyze_manifest_in`](_autosummary/wads.migration.html.md#wads.migration.analyze_manifest_in)(manifest_path)                | Analyze MANIFEST.in file and provide migration guidance for Hatchling.                                             |
|----------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------|
| [`carry_ci_env_into_pyproject`](_autosummary/wads.migration.html.md#wads.migration.carry_ci_env_into_pyproject)(ci_text, ...[, ...])  | Merge secret-backed env vars from `ci_text` into `[tool.wads.ci.env]`.                                             |
| [`dropped_infra_ci_env_vars`](_autosummary/wads.migration.html.md#wads.migration.dropped_infra_ci_env_vars)(ci_text)                | The `{ENV_VAR: SECRET_NAME}` entries [`extract_ci_env_vars()`](_autosummary/wads.migration.html.md#wads.migration.extract_ci_env_vars) drops. |
| [`extract_ci_env_vars`](_autosummary/wads.migration.html.md#wads.migration.extract_ci_env_vars)(ci_text)                      | Extract secret-backed env vars from a CI workflow's `env:` blocks.                                                 |
| [`main`](_autosummary/wads.migration.html.md#wads.migration.main)()                                            | CLI entry point for wads migration tools.                                                                          |
| [`migrate_ci_to_stub`](_autosummary/wads.migration.html.md#wads.migration.migrate_ci_to_stub)([old_ci, pin, transport, ...]) | Return the SSOT stub CI workflow that calls i2mint/wads's reusable uv-ci.                                          |
| [`migrate_ci_to_uv`](_autosummary/wads.migration.html.md#wads.migration.migrate_ci_to_uv)(old_ci, \*[, defaults])          | Migrate a CI workflow (old or 2025 format) to the uv-based template.                                               |
| [`migrate_github_ci_old_to_new`](_autosummary/wads.migration.html.md#wads.migration.migrate_github_ci_old_to_new)(old_ci[, defaults])  | Migrate old GitHub CI script to new 2025 format.                                                                   |
| [`migrate_setuptools_to_hatching`](_autosummary/wads.migration.html.md#wads.migration.migrate_setuptools_to_hatching)(setup_cfg[, ...])  | Migrate setup.cfg to pyproject.toml format using hatching.                                                         |

### Exceptions

| [`MigrationError`](_autosummary/wads.migration.html.md#wads.migration.MigrationError)   | Raised when migration cannot be completed due to missing or unmapped data.   |
|-------------------------------------------------------------------|------------------------------------------------------------------------------|

### *exception* wads.migration.MigrationError

Bases: [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

Raised when migration cannot be completed due to missing or unmapped data.

### wads.migration.analyze_manifest_in(manifest_path)

Analyze MANIFEST.in file and provide migration guidance for Hatchling.

* **Parameters:**
  **manifest_path** (`Union`[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)]) – Path to MANIFEST.in file
* **Returns:**
  - exists: bool - whether file exists
  - needs_migration: bool - whether migration is needed
  - directives: list of (command, pattern) tuples
  - recommendations: list of strings with migration guidance
  - hatchling_config: suggested pyproject.toml configuration
* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### Example

```pycon
>>> # If MANIFEST.in doesn't exist
>>> result = analyze_manifest_in('nonexistent/MANIFEST.in')
>>> result['exists']
False
>>> result['needs_migration']
False
```

### wads.migration.carry_ci_env_into_pyproject(ci_text, pyproject_path, , kind='extra', report_dropped=None)

Merge secret-backed env vars from `ci_text` into `[tool.wads.ci.env]`.

Adds only env vars not already declared (in any bucket), into the `kind`
bucket (default `extra` — available-if-set, never fails the build), and
records a `secret_aliases` entry when the env var name differs from the
backing secret. Returns the list of newly-added var names (`[]` if there
was nothing to carry). This is what makes `ci-to-stub` / `ci-to-uv`
*lossless*: secrets wired only in the old workflow YAML are preserved in
`pyproject.toml` instead of silently dropped.

Infra-secret-backed env vars (see [`extract_ci_env_vars()`](_autosummary/wads.migration.html.md#wads.migration.extract_ci_env_vars)) are the one
deliberate exception; `report_dropped` receives them as
`{ENV_VAR: SECRET_NAME}` (default: a note on stderr).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)

### wads.migration.dropped_infra_ci_env_vars(ci_text)

The `{ENV_VAR: SECRET_NAME}` entries [`extract_ci_env_vars()`](_autosummary/wads.migration.html.md#wads.migration.extract_ci_env_vars) drops.

These are env vars backed by an infra secret (publish token, GitHub
token), under their own name or an alias. A migration reports them so a
dropped `UV_PUBLISH_TOKEN` is never a silent surprise (issue #110).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

```pycon
>>> dropped_infra_ci_env_vars('''
... jobs:
...   publish:
...     steps:
...       - run: uv publish
...         env:
...           UV_PUBLISH_TOKEN: ${{ secrets.PYPI_PASSWORD }}
... ''')
{'UV_PUBLISH_TOKEN': 'PYPI_PASSWORD'}
```

### wads.migration.extract_ci_env_vars(ci_text)

Extract secret-backed env vars from a CI workflow’s `env:` blocks.

Returns a mapping `{ENV_VAR: SECRET_NAME}` for every workflow/job/step
`env:` entry whose value references `${{ secrets.X }}`. This is the
per-repo signal of which secrets the tests actually consume.

Deliberately excluded:

* Literal env vars (`PROJECT_NAME`, `LOG_LEVEL: DEBUG` …) — no secret ref.
* Infra secrets (`GITHUB_TOKEN`, `PYPI_PASSWORD`, `TEST_PYPI_PASSWORD`)
  and any other publish credential (`TWINE_PASSWORD`, `PYPI_API_TOKEN`
  …, see `wads.scripts.export_ci_env.is_publish_credential`), by env-var
  name *or* by backing secret, in any case: `UV_PUBLISH_TOKEN:
  ${{ secrets.PYPI_PASSWORD }}` in a publish step is dropped, since
  carrying it would hand the PyPI token to the test jobs (issue #110).
  [`dropped_infra_ci_env_vars()`](_autosummary/wads.migration.html.md#wads.migration.dropped_infra_ci_env_vars) lists what was dropped.
* Reusable-workflow `secrets:` pass-through blocks — that’s *transport*,
  not a signal of usage, and scanning it would re-introduce the very
  over-assignment the new model removes.

```pycon
>>> ci = '''
... env:
...   PROJECT_NAME: myproj
...   OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY || '' }}
...   HF_TOKEN: ${{ secrets.HF_WRITE_TOKEN }}
... jobs:
...   publish:
...     steps:
...       - uses: x
...         with:
...           pypi-token: ${{ secrets.PYPI_PASSWORD }}
...       - run: uv publish
...         env:
...           UV_PUBLISH_TOKEN: ${{ secrets.PYPI_PASSWORD }}
... '''
>>> extract_ci_env_vars(ci) == {
...     "OPENAI_API_KEY": "OPENAI_API_KEY", "HF_TOKEN": "HF_WRITE_TOKEN"}
True
```

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### wads.migration.main()

CLI entry point for wads migration tools.

### wads.migration.migrate_ci_to_stub(old_ci=None, , pin='@master', transport=None, trigger_mode=None, run_ci_marker=None)

Return the SSOT stub CI workflow that calls i2mint/wads’s reusable uv-ci.

The stub is ~5 lines and replaces a repo’s full inline CI with a single
`uses:` of `i2mint/wads/.github/workflows/uv-ci.yml`. All per-repo
configuration continues to come from `[tool.wads.ci.*]` in
`pyproject.toml` — the reusable workflow reads it via the
`i2mint/wads/actions/read-ci-config` action.

* **Parameters:**
  * **old_ci** (`Union`[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)]) – Optional path or content of the existing CI workflow. Used to
    locate a nearby pyproject.toml, whose `[tool.wads.ci.env]` decides
    which secrets the default named transport passes.
  * **pin** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – The wads ref the stub points at. Defaults to `"@master"`
    (floats with wads). For release-sensitive repos, pin to a tag,
    e.g. `pin="@0.2.15"` (wads tags have no `v` prefix). With the
    JSON transport the pinned ref’s `uv-ci.yml` must declare
    `WADS_CI_SECRETS_JSON` (releases after 0.2.14) — pinning an
    older tag produces a workflow GitHub rejects at parse time, so a
    warning is emitted for any non-master JSON pin. Must start with
    `"@"`.
  * **transport** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – `None` (default) writes the named transport — converting
    a JSON-transport stub at `old_ci` — unless the repo declares
    secret names outside the wads superset, in which case it writes
    the JSON transport (a named stub passing them could not start)
    and says so on stderr. `"named"` passes an explicit subset
    (PYPI_PASSWORD + the [tool.wads.ci.env]-declared secrets); every
    name must be in the wads superset or GitHub rejects the workflow
    at parse time (issue #63) — out-of-superset names trigger a loud
    warning. `"json"` (opt-in) passes the repo’s whole secrets
    context as one `WADS_CI_SECRETS_JSON` secret, so any secret name
    works, but every secret the repo can read reaches the called
    workflow and GitHub’s malicious-workflow scanner may hold its runs
    (`action_required`, zero jobs; issues #74, #88, #101), which is
    why it is not the default.
  * **trigger_mode** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – `"auto"` or `"on-demand"`. `None` (default) takes
    `[tool.wads.ci.trigger].mode` from the pyproject.toml of the repo
    holding `old_ci` (`"auto"` when there is none). On-demand stubs
    run nothing unless the commit subject carries the run-ci marker or
    the run is a `workflow_dispatch`; see [`wads.ci_trigger`](_autosummary/wads.ci_trigger.html.md#module-wads.ci_trigger).
  * **run_ci_marker** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – The on-demand marker; `None` takes it from the same
    pyproject (default `"[run ci]"`).
* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)
* **Returns:**
  The stub workflow content as a string.

### Example

```pycon
>>> stub = migrate_ci_to_stub()
>>> 'i2mint/wads/.github/workflows/uv-ci.yml@master' in stub
True
>>> 'PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }}' in stub
True
>>> 'toJSON(secrets)' in stub
False
>>> pinned = migrate_ci_to_stub(pin='@0.2.15')
>>> 'uv-ci.yml@0.2.15' in pinned
True
>>> as_json = migrate_ci_to_stub(transport='json')
>>> 'WADS_CI_SECRETS_JSON: ${{ toJSON(toJSON(secrets)) }}' in as_json
True
```

### wads.migration.migrate_ci_to_uv(old_ci, , defaults=None)

Migrate a CI workflow (old or 2025 format) to the uv-based template.

Since the uv template reads all configuration from pyproject.toml
(via the read-ci-config action), the migration is straightforward:
replace the workflow file with the uv template.

* **Parameters:**
  * **old_ci** (`Union`[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)]) – Path to the existing CI workflow file, or its content as a string.
  * **defaults** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]) – Optional dict with ‘project_name’ if it cannot be extracted
    from the old CI file.
* **Returns:**
  The uv CI template content as a string.

### Example

```pycon
>>> result = migrate_ci_to_uv('name: CI\non: push')
>>> 'astral-sh/setup-uv' in result
True
>>> 'build-dist-uv' in result
True
```

### wads.migration.migrate_github_ci_old_to_new(old_ci, defaults=None)

Migrate old GitHub CI script to new 2025 format.

* **Parameters:**
  * **old_ci** (`Union`[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)]) – Path to old CI file or its content as string
  * **defaults** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]) – Default values for missing fields (e.g., {‘project_name’: ‘myproject’})
* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)
* **Returns:**
  String content of the new CI script
* **Raises:**
  [**MigrationError**](_autosummary/wads.migration.html.md#wads.migration.MigrationError) – If unmapped elements exist or required fields are missing

#### NOTE
The 2025 CI template is fully config-driven via pyproject.toml.
Migration now primarily involves setting up proper [tool.wads.ci] configuration
rather than doing template substitution. For new projects, use populate_pkg_dir()
to generate proper configuration.

### Example

```pycon
>>> # Migration is now about configuration, not template substitution
>>> # For new projects, use populate_pkg_dir() instead
>>> # Old migration approach with placeholders is deprecated
>>> pass
```

### wads.migration.migrate_setuptools_to_hatching(setup_cfg, defaults=None, , rules=None)

Migrate setup.cfg to pyproject.toml format using hatching.

* **Parameters:**
  * **setup_cfg** (`Union`[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Mapping`](https://docs.python.org/3/library/typing.html#typing.Mapping)]) – Either a file path, file content string, or dict of setup.cfg
  * **defaults** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]) – Default values to use for missing required fields
  * **rules** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]) – Custom transformation rules (defaults to setup_cfg_to_pyproject_toml_rules)
* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)
* **Returns:**
  String content of the generated pyproject.toml
* **Raises:**
  [**MigrationError**](_autosummary/wads.migration.html.md#wads.migration.MigrationError) – If required fields are missing or unmapped data exists

### Example

```pycon
>>> # From file path
>>> pyproject = migrate_setuptools_to_hatching('setup.cfg')
>>>
>>> # From dict with all required fields
>>> cfg = {
...     'metadata': {
...         'name': 'myproj',
...         'version': '0.1.0',
...         'description': 'My project',
...         'url': 'https://github.com/user/myproj',
...         'license': 'MIT'
...     }
... }
>>> result = migrate_setuptools_to_hatching(cfg)
>>> 'name = "myproj"' in result
True
>>>
>>> # With defaults for missing fields
>>> minimal = {'metadata': {'name': 'test', 'version': '1.0'}}
>>> result = migrate_setuptools_to_hatching(
...     minimal,
...     defaults={
...         'description': 'Test project',
...         'url': 'https://test.com',
...         'license': 'MIT'
...     }
... )
>>> 'name = "test"' in result
True
```


# _autosummary/wads.npm_config.html.md

# wads.npm_config

Read NPM CI configuration from a `package.json` `wads.ci` block.

This is the NPM-side analog of [`wads.ci_config.CIConfig`](_autosummary/wads.ci_config.html.md#wads.ci_config.CIConfig) (which reads
`[tool.wads.ci]` from `pyproject.toml`). npm ignores unknown top-level keys,
so wads namespaces its config under a top-level `"wads"` key:

```json
{
  "name": "@scope/my-widget",
  "wads": {
    "ci": {
      "subdir": "js",
      "nodeVersions": ["20", "22", "24"],
      "publish": {"enabled": false, "marker": "[publish-npm]"}
    }
  }
}
```

The reusable workflow (`i2mint/wads/.github/workflows/npm-ci.yml`) parses the
same block in CI via `node`; this class is the parsing/validation surface used
from Python (tests, `populate` validation, inspection tooling).

### Module Attributes

| [`NPM_CI_DEFAULTS`](_autosummary/wads.npm_config.html.md#wads.npm_config.NPM_CI_DEFAULTS)            | Defaults applied when a field is absent from `wads.ci`.           |
|-----------------------------------------------------------------------------|-------------------------------------------------------------------|
| [`SUPPORTED_PACKAGE_MANAGERS`](_autosummary/wads.npm_config.html.md#wads.npm_config.SUPPORTED_PACKAGE_MANAGERS) | Package managers the reusable npm CI workflow knows how to drive. |
| [`NPM_PUBLISH_DEFAULTS`](_autosummary/wads.npm_config.html.md#wads.npm_config.NPM_PUBLISH_DEFAULTS)       | Defaults for the nested `wads.ci.publish` block.                  |
| [`TRIGGER_MODES`](_autosummary/wads.npm_config.html.md#wads.npm_config.TRIGGER_MODES)              | `wads.ci.trigger.mode` values the reusable workflow understands.  |
| [`NPM_TRIGGER_DEFAULTS`](_autosummary/wads.npm_config.html.md#wads.npm_config.NPM_TRIGGER_DEFAULTS)       | Defaults for the nested `wads.ci.trigger` block.                  |

### Classes

| [`NpmCIConfig`](_autosummary/wads.npm_config.html.md#wads.npm_config.NpmCIConfig)(package_json_data)   | Typed accessors over a `package.json` `wads.ci` configuration block.   |
|-----------------------------------------------------------------------------------|------------------------------------------------------------------------|

### wads.npm_config.NPM_CI_DEFAULTS *= {'buildCommand': 'npm run build --if-present', 'lintCommand': 'npm run lint --if-present', 'nodeVersions': ['20', '22', '24'], 'packageManager': 'npm', 'publishNode': '24', 'subdir': 'js', 'testCommand': 'npm test --if-present'}*

Defaults applied when a field is absent from `wads.ci`.

`packageManager` is `"npm"` here for the typed Python accessor, but in CI
an *absent* field triggers auto-detection (a `pnpm-lock.yaml` in the
package dir selects pnpm); an explicit value always wins. So existing npm
consumers — no field, no `pnpm-lock.yaml` — keep byte-identical behavior.

### wads.npm_config.NPM_PUBLISH_DEFAULTS *= {'access': 'public', 'enabled': False, 'marker': '[publish-npm]', 'provenance': True, 'registry': 'https://registry.npmjs.org', 'trustedPublishing': True}*

Defaults for the nested `wads.ci.publish` block.

### wads.npm_config.NPM_TRIGGER_DEFAULTS *= {'mode': 'auto', 'runCiMarker': '[run ci]'}*

Defaults for the nested `wads.ci.trigger` block. Same values as the Python
side (`wads.ci_config.DFLT_TRIGGER_MODE` / `DFLT_RUN_CI_MARKER`) so a repo
with both a pyproject.toml and a package.json can use one marker convention.

### *class* wads.npm_config.NpmCIConfig(package_json_data)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Typed accessors over a `package.json` `wads.ci` configuration block.

#### *classmethod* from_file(package_json_path)

Build from a `package.json` file path (file or its directory).

* **Return type:**
  [`NpmCIConfig`](_autosummary/wads.npm_config.html.md#wads.npm_config.NpmCIConfig)

#### has_ci_config()

True if the package.json declares a `wads.ci` block.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

#### *property* package_manager *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

The package manager driving install/build (`"npm"` or `"pnpm"`).

Returns the explicit `wads.ci.packageManager` if set, else the default
(`"npm"`). Note the CI workflow additionally *auto-detects* pnpm from a
`pnpm-lock.yaml` when the field is absent — this accessor reports only
the declared value.

#### *property* run_ci_marker *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

Commit-subject substring that runs CI in `"on-demand"` mode.

Defaults to `"[run ci]"` (same default as the Python side). Must be a
non-empty single line: an empty marker would match every subject and
silently turn on-demand back into auto.

#### *property* trigger_mode *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

`"auto"` (default) or `"on-demand"`.

`"on-demand"` means nothing runs unless the commit *subject line*
contains [`run_ci_marker`](_autosummary/wads.npm_config.html.md#wads.npm_config.NpmCIConfig.run_ci_marker), or the run is a manual
`workflow_dispatch`. Raises `ValueError` on any other value.

```pycon
>>> NpmCIConfig({}).trigger_mode
'auto'
>>> NpmCIConfig({"wads": {"ci": {"trigger": {"mode": "on-demand"}}}}).trigger_mode
'on-demand'
```

* **Type:**
  When CI runs

### wads.npm_config.SUPPORTED_PACKAGE_MANAGERS *= ('npm', 'pnpm')*

Package managers the reusable npm CI workflow knows how to drive.

### wads.npm_config.TRIGGER_MODES *= ('auto', 'on-demand')*

`wads.ci.trigger.mode` values the reusable workflow understands. Mirrors
`wads.ci_config.TRIGGER_MODES` (the Python/pyproject.toml side).


# _autosummary/wads.pack.html.md

# wads.pack

Utils to package and publish.

The typical sequence of the methodic and paranoid could be something like this:

```default
python pack.py current-configs  # see what you got
python pack.py increment-configs-version  # update (increment the version and write that in setup.cfg
python pack.py current-configs-version  # see that it worked
python pack.py current-configs  # ... if you really want to see the whole configs again (you're really paranoid)
python pack.py run-setup  # see that it worked
python pack.py twine-upload-dist  # publish
# and then go check things work...
```

If you’re crazy (or know what you’re doing) just do

```default
python pack.py go
```

### Module Attributes

| [`COMMANDS`](_autosummary/wads.pack.html.md#wads.pack.COMMANDS)   | The commands `pack` exposes, in the order they appear in `pack --help`.   |
|-------------------------------------------------------------|---------------------------------------------------------------------------|

### Functions

| [`check_in`](_autosummary/wads.pack.html.md#wads.pack.check_in)(commit_message, \*[, work_tree, ...])     | Validate, normalize, stage, commit and push your local changes to a remote repository.                                            |
|-----------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------|
| `clog`(condition, \*args[, log_func])                                                               |                                                                                                                                   |
| `current_configs`(pkg_dir)                                                                          |                                                                                                                                   |
| `current_configs_version`(pkg_dir)                                                                  |                                                                                                                                   |
| [`current_pypi_version`](_autosummary/wads.pack.html.md#wads.pack.current_pypi_version)(pkg_dir, \*[, name, ...])     | Return version of package on pypi.python.org using json.                                                                          |
| `delete_pkg_directories`(pkg_dir[, verbose])                                                        |                                                                                                                                   |
| [`extract_pkg_dir_and_name`](_autosummary/wads.pack.html.md#wads.pack.extract_pkg_dir_and_name)(pkg_spec, \*[, validate]) | Extracts the pkg_dir and pkg_dirname from the input `pkg_spec`.                                                                   |
| [`folders_that_have_init_py_files`](_autosummary/wads.pack.html.md#wads.pack.folders_that_have_init_py_files)(pkg_dir)           | Get a list of folders in the package directory that have an \_\_init_\_.py file.                                                  |
| `generate_and_publish_docs`(pkg_dir, \*[, ...])                                                     |                                                                                                                                   |
| [`get_module_path`](_autosummary/wads.pack.html.md#wads.pack.get_module_path)(module)                            | Get the path to the directory containing the module's code file.                                                                  |
| [`get_name_from_configs`](_autosummary/wads.pack.html.md#wads.pack.get_name_from_configs)(pkg_dir, \*[, ...])          | Get name from local config file (pyproject.toml or setup.cfg)                                                                     |
| [`get_pkg_name`](_autosummary/wads.pack.html.md#wads.pack.get_pkg_name)(pkg_spec[, validate])                 | Get the name of the package from a package name, module object or path.                                                           |
| `git_commit_and_push`(pkg_dir, \*[, version, ...])                                                  |                                                                                                                                   |
| [`go`](_autosummary/wads.pack.html.md#wads.pack.go)(pkg_dir, \*[, version, publish_docs_to, ...])   | Update version, package and deploy: Runs in a sequence: increment_configs_version, update_setup_cfg, run_setup, twine_upload_dist |
| [`goo`](_autosummary/wads.pack.html.md#wads.pack.goo)(pkg_dir, commit_message, \*[, git_dir, ...])   | Validate, normalize, stage, commit and push your local changes to a remote repository.                                            |
| [`highest_pypi_version`](_autosummary/wads.pack.html.md#wads.pack.highest_pypi_version)(pkg_dir, \*[, name, ...])     | Return version of package on pypi.python.org using json.                                                                          |
| `highest_tag_version`(pkg_spec[, ...])                                                              |                                                                                                                                   |
| [`http_get_json`](_autosummary/wads.pack.html.md#wads.pack.http_get_json)(url[, use_requests])                 | Make ah http request to url and get json, and return as python dict                                                               |
| [`increment_configs_version`](_autosummary/wads.pack.html.md#wads.pack.increment_configs_version)(pkg_dir, \*[, version])  | Increment version in config file (pyproject.toml and/or setup.cfg).                                                               |
| `increment_version`(version_str)                                                                    |                                                                                                                                   |
| [`main`](_autosummary/wads.pack.html.md#wads.pack.main)()                                             | Entry point of the `pack` console script.                                                                                         |
| `next_version_for_package`(pkg_dir[, name, ...])                                                    |                                                                                                                                   |
| `pjoin`(\*p)                                                                                        |                                                                                                                                   |
| [`postprocess_ini_section_items`](_autosummary/wads.pack.html.md#wads.pack.postprocess_ini_section_items)(items)               | Transform newline-separated string values into actual list of strings (assuming that intent)                                      |
| [`preprocess_ini_section_items`](_autosummary/wads.pack.html.md#wads.pack.preprocess_ini_section_items)(items)                | Transform list values into newline-separated strings, in view of writing the value to a ini formatted section                     |
| [`process_missing_module_docstrings`](_autosummary/wads.pack.html.md#wads.pack.process_missing_module_docstrings)(\*, pkg_dir)     | Goes through modules of package, sees which ones don't have docstrings, and gives you the option to write one.                    |
| [`pyproject_toml_version`](_autosummary/wads.pack.html.md#wads.pack.pyproject_toml_version)(pkg_spec)                   | Get version from pyproject.toml file.                                                                                             |
| `raise_error`(msg[, error_type])                                                                    |                                                                                                                                   |
| [`read_and_resolve_setup_configs`](_autosummary/wads.pack.html.md#wads.pack.read_and_resolve_setup_configs)(pkg_dir, \*[, ...]) | make setup params and call setup                                                                                                  |
| `read_configs`(pkg_dir[, postproc, section, ...])                                                   |                                                                                                                                   |
| [`run_setup`](_autosummary/wads.pack.html.md#wads.pack.run_setup)(pkg_dir)                                 | Run `python -m build` (modern PEP 517 compliant build)                                                                            |
| [`set_version`](_autosummary/wads.pack.html.md#wads.pack.set_version)(pkg_dir, version)                      | Update version in config file (pyproject.toml and/or setup.cfg)                                                                   |
| [`setup_cfg_version`](_autosummary/wads.pack.html.md#wads.pack.setup_cfg_version)(pkg_spec)                        | Get version from setup.cfg file.                                                                                                  |
| [`sorted_versions`](_autosummary/wads.pack.html.md#wads.pack.sorted_versions)(strings[, version_patch_prefix])   | Filter out and return version strings in (versioning) descending order.                                                           |
| [`twine_upload_dist`](_autosummary/wads.pack.html.md#wads.pack.twine_upload_dist)(pkg_dir, \*[, options_str])      | Publish to pypi.                                                                                                                  |
| [`update_setup_cfg`](_autosummary/wads.pack.html.md#wads.pack.update_setup_cfg)(pkg_dir, \*[, new_deploy, ...])   | Update setup.cfg or pyproject.toml.                                                                                               |
| [`validate_package_name`](_autosummary/wads.pack.html.md#wads.pack.validate_package_name)(name[, raise_error])         | Validate that a package name follows PEP 508 naming conventions.                                                                  |
| [`validate_versions`](_autosummary/wads.pack.html.md#wads.pack.validate_versions)(versions[, ...])                 | Validate versions from different sources.                                                                                         |
| `versions_from_different_sources`(pkg_spec)                                                         |                                                                                                                                   |
| [`versions_from_pypi`](_autosummary/wads.pack.html.md#wads.pack.versions_from_pypi)(pkg_dir, \*[, name, ...])       | Return version of package on pypi.python.org using json.                                                                          |
| `versions_from_tags`(pkg_spec[, ...])                                                               |                                                                                                                                   |
| `write_configs`(pkg_dir, configs[, preproc, ...])                                                   |                                                                                                                                   |

### wads.pack.COMMANDS *= [<function generate_and_publish_docs>, <function current_configs>, <function increment_configs_version>, <function current_configs_version>, <function twine_upload_dist>, <function read_and_resolve_setup_configs>, <function update_setup_cfg>, <function go>, <function goo>, <function check_in>, <function get_name_from_configs>, <function run_setup>, <function current_pypi_version>, <function extract_pkg_dir_and_name>, <function git_commit_and_push>, <function process_missing_module_docstrings>]*

The commands `pack` exposes, in the order they appear in `pack --help`.

### wads.pack.check_in(commit_message, , work_tree='.', git_dir=None, auto_choose_default_action=False, bypass_docstring_validation=False, bypass_tests=False, bypass_code_formatting=False, verbose=False, pre_git_hooks=())

Validate, normalize, stage, commit and push your local changes to a remote repository.

* **Parameters:**
  * **commit_message** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Your commit message
  * **work_tree** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – The relative or absolute path of the working directory. Defaults to ‘.’.
  * **git_dir** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – The relative or absolute path of the git directory. If None, it will be taken to be “{work_tree}/.git/”. Defaults to None.
  * **auto_choose_default_action** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Set to True if you don’t want to be prompted and automatically select the default action. Defaults to False.
  * **bypass_docstring_validation** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Set to True if you don’t want to check if a docstring exists for every module, class and function. Defaults to False.
  * **bypass_tests** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Set to True if you don’t want to run doctests and other tests. Defaults to False.
  * **bypass_code_formatting** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Set to True if you don’t want the code to be automatically formatted using axblack. Defaults to False.
  * **verbose** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Set to True if you want to log extra information during the process. Defaults to False.
  * **pre_git_hooks** ([`Sequence`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Sequence)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – A sequence of git commands to run before the git commit. Defaults to ().

### wads.pack.current_pypi_version(pkg_dir, , name=None, use_requests=True)

Return version of package on pypi.python.org using json.

Routes through [`http_get_json()`](_autosummary/wads.pack.html.md#wads.pack.http_get_json), so it works whether or not the optional
`requests` dependency is installed (urllib fallback), and returns `None`
when the package isn’t on PyPI instead of raising.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

```pycon
>>> current_pypi_version('wads')
'0.1.19'
```

### wads.pack.extract_pkg_dir_and_name(pkg_spec, , validate=True)

Extracts the pkg_dir and pkg_dirname from the input `pkg_spec`.
Optionally validates the pkg_dir is actually one (has a pkg_name/_\_init_\_.py file)

Also processes input to get a path from a pathlib.Path object, or a module object,
or a module/package name string.

`pkg_spec` can be an imported package (must be a locally developped package)
whose name and containing directory is the same):

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> import os, wads
>>> pkg_dir, pkg_name = extract_pkg_dir_and_name(wads)
>>> pkg_name
'wads'
>>> os.path.isfile(os.path.join(pkg_dir, pkg_name, '__init__.py'))
True
```

You can also just specify the name of the package (it will be imported):

```pycon
>>> extract_pkg_dir_and_name('wads')
('.../wads', 'wads')
```

Or you can specify the path to the package directory explicitly:

```pycon
>>> extract_pkg_dir_and_name('/home/user/projects/wads')
('/home/user/projects/wads', 'wads')
```

### wads.pack.folders_that_have_init_py_files(pkg_dir)

Get a list of folders in the package directory that have an \_\_init_\_.py file.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> folders_that_have_init_py_files('/home/user/projects/wads')
['wads', 'wads/util', 'wads/pack', 'wads/docs_gen']
```

### wads.pack.get_module_path(module)

Get the path to the directory containing the module’s code file.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> import os
>>> get_module_path(os)
'/usr/lib/python3.8'
>>> import sklearn
>>> get_module_path(sklearn)
'/usr/local/lib/python3.8/dist-packages/sklearn'
>>> import local_package
>>> get_module_path(local_package)
'/home/user/projects/local_package/local_package'
```

Read more: [https://github.com/i2mint/wads/discussions/7#discussioncomment-9761632](https://github.com/i2mint/wads/discussions/7#discussioncomment-9761632)

### wads.pack.get_name_from_configs(pkg_dir, , assert_exists=True, validate_name=True)

Get name from local config file (pyproject.toml or setup.cfg)

### wads.pack.get_pkg_name(pkg_spec, validate=True)

Get the name of the package from a package name, module object or path.
Optionally validates some naming rules.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### wads.pack.go(pkg_dir, , version=None, publish_docs_to=None, verbose=True, skip_git_commit=False, answer_yes_to_all_prompts=False, twine_upload_options_str='', keep_dist_pkgs=False, commit_message='')

Update version, package and deploy:
Runs in a sequence: increment_configs_version, update_setup_cfg, run_setup, twine_upload_dist

* **Parameters:**
  * **version** – The desired version (if not given, will increment the current version
  * **verbose** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Whether to print stuff or not
  * **skip_git_commit** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Whether to skip the git commit and push step
  * **answer_yes_to_all_prompts** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – If you do git commit and push, whether to ask confirmation after showing status

### wads.pack.goo(pkg_dir, commit_message, , git_dir=None, auto_choose_default_action=False, bypass_docstring_validation=False, bypass_tests=False, bypass_code_formatting=False, verbose=False)

Validate, normalize, stage, commit and push your local changes to a remote repository.

* **Parameters:**
  * **pkg_dir** ([*str*](https://docs.python.org/3/builtins/stdtypes.html#str) *,* *optional*) – The relative or absolute path of the working directory. Defaults to ‘.’.
  * **commit_message** ([*str*](https://docs.python.org/3/builtins/stdtypes.html#str)) – Your commit message
  * **git_dir** ([*str*](https://docs.python.org/3/builtins/stdtypes.html#str) *,* *optional*) – The relative or absolute path of the git directory. If None, it will be taken to be “{work_tree}/.git/”. Defaults to None.
  * **auto_choose_default_action** ([*bool*](https://docs.python.org/3/builtins/functions.html#bool) *,* *optional*) – Set to True if you don’t want to be prompted and automatically select the default action. Defaults to False.
  * **bypass_docstring_validation** ([*bool*](https://docs.python.org/3/builtins/functions.html#bool) *,* *optional*) – Set to True if you don’t want to check if a docstring exists for every module, class and function. Defaults to False.
  * **bypass_tests** ([*bool*](https://docs.python.org/3/builtins/functions.html#bool) *,* *optional*) – Set to True if you don’t want to run doctests and other tests. Defaults to False.
  * **bypass_code_formatting** ([*bool*](https://docs.python.org/3/builtins/functions.html#bool) *,* *optional*) – Set to True if you don’t want the code to be automatically formatted using axblack. Defaults to False.
  * **verbose** ([*bool*](https://docs.python.org/3/builtins/functions.html#bool) *,* *optional*) – Set to True if you want to log extra information during the process. Defaults to False.

### wads.pack.highest_pypi_version(pkg_dir, , name=None, use_requests=True)

Return version of package on pypi.python.org using json.

```pycon
>>> highest_pypi_version('wads')
'0.1.19'
```

* **Parameters:**
  **package** – Name of the package
* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]
* **Returns:**
  A version (string) or None if there was an exception (usually means there

### wads.pack.http_get_json(url, use_requests=True)

Make ah http request to url and get json, and return as python dict

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### wads.pack.increment_configs_version(pkg_dir, , version=None)

Increment version in config file (pyproject.toml and/or setup.cfg).

### wads.pack.main()

Entry point of the `pack` console script.

### wads.pack.postprocess_ini_section_items(items)

Transform newline-separated string values into actual list of strings (assuming that intent)

* **Return type:**
  [`Generator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Generator)

```pycon
>>> section_from_ini = {
...     'name': 'wads',
...     'keywords': '\n\tpackaging\n\tpublishing'
... }
>>> section_for_python = dict(postprocess_ini_section_items(section_from_ini))
>>> section_for_python
{'name': 'wads', 'keywords': ['packaging', 'publishing']}
```

### wads.pack.preprocess_ini_section_items(items)

Transform list values into newline-separated strings, in view of writing the value to a ini formatted section

* **Return type:**
  [`Generator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Generator)

```pycon
>>> section = {
...     'name': 'wads',
...     'keywords': ['documentation', 'packaging', 'publishing']
... }
>>> for_ini = dict(preprocess_ini_section_items(section))
>>> print('keywords =' + for_ini['keywords'])
keywords =
    documentation
    packaging
    publishing
```

### wads.pack.process_missing_module_docstrings(, pkg_dir, action='input', exceptions=(), docstr_template='"""\\\\n{user_input}\\\\n"""\\\\n')

Goes through modules of package, sees which ones don’t have docstrings,
and gives you the option to write one.

The function will go through all .py files (except those mentioned in exceptions),
check if there’s a module docstring (that is, that the first non-white characters are
triple-quotes (double or single)).

What happens with that depends on the `action` argument,

If `action='list'`, those modules missing docstrings will be listed.
If `action='count'`, the count of those modules missing docstrings will be returned.

If `action='input'`, for every module you’ll be given the option to enter a
SINGLE LINE module docstring (though you could include multi-lines with n).
Just type the docstring you want and hit enter to go to the next module with missing docstring.
Or, you can also:

> - type exit, or e: To exit this process
> - type skip, or s: To skip the module and go to the next
> - type funcs, or f: To see a print out of functions, classes, and methods
> - just hit enter, to get some lines of code (the first, then the next, etc.)
> - enter a number, or a number:number, to specify what lines you want to see printed

Printing lines helps you write an informed module docstring

:keyword module, docstr, docstrings, module doc strings

### wads.pack.pyproject_toml_version(pkg_spec)

Get version from pyproject.toml file.

### wads.pack.read_and_resolve_setup_configs(pkg_dir, , new_deploy=False, version=None, assert_names=True)

make setup params and call setup

* **Parameters:**
  * **pkg_dir** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Directory where the pkg is (which is also where the setup.cfg is)
  * **new_deploy** – whether this setup for a new deployment (publishing to pypi) or not
  * **version** – The version number to set this up as.
    If not given will look at setup.cfg[metadata] for one,
    and if not found there will use the current version (requesting pypi.org)
    and bump it if the new_deploy flag is on

### wads.pack.run_setup(pkg_dir)

Run `python -m build` (modern PEP 517 compliant build)

### wads.pack.set_version(pkg_dir, version)

Update version in config file (pyproject.toml and/or setup.cfg)

### wads.pack.setup_cfg_version(pkg_spec)

Get version from setup.cfg file.

### wads.pack.sorted_versions(strings, version_patch_prefix='')

Filter out and return version strings in (versioning) descending order.

### wads.pack.twine_upload_dist(pkg_dir, , options_str=None)

Publish to pypi. Runs `python -m twine upload dist/*`

### wads.pack.update_setup_cfg(pkg_dir, , new_deploy=False, version=None, verbose=True)

Update setup.cfg or pyproject.toml.
If version is not given, will ask pypi (via http request) what the current version
is, and increment that.

### wads.pack.validate_package_name(name, raise_error=True)

Validate that a package name follows PEP 508 naming conventions.

Package names must:

- Begin and end with ASCII letters or digits
- Contain only ASCII letters, digits, underscores, hyphens, and periods

* **Parameters:**
  * **name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – The package name to validate
  * **raise_error** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – If True, raises ValueError on invalid name. If False, returns bool.
* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)
* **Returns:**
  True if valid, False if invalid (when raise_error=False)
* **Raises:**
  [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – If name is invalid and raise_error=True

### Examples

```pycon
>>> validate_package_name('wads')
True
>>> validate_package_name('wads-test')
True
>>> validate_package_name('wads_test')
True
>>> validate_package_name('_wads_test', raise_error=False)
False
>>> validate_package_name('wads-test-', raise_error=False)
False
```

### wads.pack.validate_versions(versions, action_when_not_valid=<function raise_error>)

Validate versions from different sources.

You get the versions input from the `versions_from_different_sources` function.

* **Parameters:**
  * **versions** ([`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)) – A dictionary with the versions from different sources
  * **action_when_not_valid** – A function that will be called when the versions are not valid
    Default is to raise a ValueError with the error message.
    Another option is to print the error message, log it, or issue a warning.
* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)
* **Returns:**
  The versions if they are valid

### wads.pack.versions_from_pypi(pkg_dir, , name=None, use_requests=True)

Return version of package on pypi.python.org using json.

* **Parameters:**
  **package** – Name of the package
* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)
* **Returns:**
  A version (string) or None if there was an exception (usually means there


# _autosummary/wads.populate.html.md

# wads.populate

Populate a package directory with useful packaging files.

Provides the `populate_pkg_dir` function to add standard files like
README.md, pyproject.toml, and .gitignore.

### Functions

| [`cd`](_autosummary/wads.populate.html.md#wads.populate.cd)(newdir[, verbose])                            | Change your working directory, do stuff, and change back to the original         |
|---------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------|
| `clog`(\*args[, condition, log_func])                                                             |                                                                                  |
| `gen_readme_text`(name[, text])                                                                   |                                                                                  |
| [`get_github_project_description`](_autosummary/wads.populate.html.md#wads.populate.get_github_project_description)(repo[, ...])      | Get a project's description from GitHub, falling back to a default.              |
| [`main`](_autosummary/wads.populate.html.md#wads.populate.main)()                                           | Entry point of the `populate` console script.                                    |
| [`populate_pkg_dir`](_autosummary/wads.populate.html.md#wads.populate.populate_pkg_dir)(pkg_dir[, version, ...])        | Populate project directory root with useful packaging files, if they're missing. |
| [`populate_proj_from_url`](_autosummary/wads.populate.html.md#wads.populate.populate_proj_from_url)(url[, proj_rootdir, ...]) | git clone a repository and set the resulting folder up for packaging.            |
| [`update_pack_and_setup_py`](_autosummary/wads.populate.html.md#wads.populate.update_pack_and_setup_py)(target_pkg_dir[, ...])  | Just copy over setup.py and pack.py (moving the original to be prefixed by '_'   |
| [`write_pyproject_configs`](_autosummary/wads.populate.html.md#wads.populate.write_pyproject_configs)(pkg_dir, configs)        | Write pyproject.toml file from template and configs.                             |

### Classes

| [`PopulateTracker`](_autosummary/wads.populate.html.md#wads.populate.PopulateTracker)()   | Track actions during populate for summary reporting.   |
|----------------------------------------------------------------------|--------------------------------------------------------|

### *class* wads.populate.PopulateTracker

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Track actions during populate for summary reporting.

#### add(filename)

Record a file that was added.

#### attention(filename, reason=None)

Record a file that needs attention with optional reason.

#### error(filename, error)

Record a file operation that failed.

#### print_summary(verbose=True)

Print emoji-based summary of what happened.

#### skip(filename)

Record a file that was skipped (already exists).

### wads.populate.cd(newdir, verbose=True)

Change your working directory, do stuff, and change back to the original

### wads.populate.get_github_project_description(repo, default_factory=<function \_mk_default_project_description>, \*, token=None, use_gh_cli=True)

Get a project’s description from GitHub, falling back to a default.

Resolution order (each step is skipped when it yields nothing):

1. **GitHub REST API.** A token (the `token` argument, else the
   `GITHUB_TOKEN` / `GH_TOKEN` env var) is sent as a bearer token so
   private repositories you can access are reachable. Without a token the
   API only sees public repos – private repos return 404.
2. \*\*The `gh` CLI\*\* (`gh repo view`), if installed and authenticated –
   the easiest way to reach private repos without managing a token.
3. `default_factory(org/proj)` – a placeholder description.

Never raises on a fetch failure: a missing or unreachable description should
not block project creation (a genuinely missing repo surfaces clearly at the
subsequent `git clone`).

### wads.populate.main()

Entry point of the `populate` console script.

### wads.populate.populate_pkg_dir(pkg_dir, version='0.0.1', description='There is a bit of an air of mystery around this project...', , root_url=None, author=None, license='mit', description_file='README.md', keywords=[], install_requires=[], long_description='file:README.md', long_description_content_type='text/markdown', include_pip_install_instruction_in_readme=True, verbose=True, overwrite=(), defaults_from=None, create_docsrc=False, skip_docsrc_gen=False, skip_ci_def_gen=False, migrate=False, create_gitattributes=True, create_setup_py=False, create_community_files=False, version_control_system=None, ci_def_path=None, ci_tpl_path=None, project_type='lib', frontend='', with_npm=False, npm_subdir='js', npm_package_name=None, npm_version='0.0.1', npm_package_manager='npm', create_tests=False, \*\*configs)

Populate project directory root with useful packaging files, if they’re missing.

```pycon
>>> from wads.populate import populate_pkg_dir
>>> import os
>>> name = 'wads'
>>> pkg_dir = f'/path/to/projects/{name}'
>>> populate_pkg_dir(pkg_dir,
...                  description='Tools for packaging',
...                  root_url=f'https://github.com/i2mint',
...                  author='OtoSense')
```

* **Parameters:**
  * **pkg_dir** ([*str*](https://docs.python.org/3/builtins/stdtypes.html#str) *,* *optional*) – The relative or absolute path of the working directory. Defaults to ‘.’.
  * **version** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – The desired version
  * **description** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Short description of project
  * **root_url** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Root url of the code repository (not the url of the project, but one level up that!)
  * **author** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Author of the package
  * **license** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – License name for the package (should be recognized by pypi). Default is ‘mit’
  * **description_file** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – File name containing a description of the project. Default is ‘README.md’
  * **keywords** ([`list`](https://docs.python.org/3/builtins/stdtypes.html#list) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Keywords to include in pypi publication
  * **install_requires** ([`list`](https://docs.python.org/3/builtins/stdtypes.html#list) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – The (pip install) names of of the packages required to install the package we’re generating
  * **long_description** – Text of the long description. Default is “[file:README.md](file:README.md)” (takes contents of README.md)
  * **long_description_content_type** – How to parse the long_description. Default is “text/markdown”
  * **verbose** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Set to True if you want to log extra information during the process. Defaults to False.
  * **default_from** – Name of field to look up in wads_configs to get defaults from,
    or ‘user_input’ to get it from user input.
  * **skip_docsrc_gen** – Skip the generation of documentation stuff
  * **create_docsrc** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – If True, create and populate a `docsrc/` directory (overrides skip_docsrc_gen).
  * **skip_ci_def_gen** – Skip the generation of the CI stuff
  * **create_setup_py** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – If True, create setup.py for backward compatibility (default: False, not needed with Hatchling).
  * **create_community_files** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – If True, create community files (.editorconfig, issue/PR templates, dependabot.yml). Default: False.
  * **migrate** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – If True, migrate existing setup.cfg to pyproject.toml and old CI to new CI format.
    Will fail if old CI has unmappable content.
  * **create_gitattributes** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – If True (default), create a .gitattributes file with ‘

    ```
    *
    ```

    .ipynb linguist-documentation’.
  * **version_control_system** – ‘github’ or ‘gitlab’ (will TRY to be resolved from root url if not given)
  * **ci_def_path** – Path of the CI definition
  * **ci_tpl_path** – Pater of the template definition
  * **frontend** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Comma-separated list of frontend profiles to add (issue
    #39), e.g. `"ts"` or `"js,ts"`. Available profiles: `js` (the
    original npm overlay), `ts` (single-package TypeScript), and
    `ts-monorepo` (pnpm workspaces + turbo). Each component lands in its
    own subdir with its own path-filtered workflow, so multiple profiles
    never collide. Empty by default. The single-component overrides below
    (`npm_subdir` / `npm_package_name` / `npm_package_manager`) apply
    only when exactly one profile is selected; with several, each profile
    uses its own defaults.
  * **with_npm** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Back-compat alias for `frontend="js"` (issue #32). If
    True, adds the `js` profile: a `<npm_subdir>/package.json` (with a
    `wads.ci` config block) and a `.github/workflows/npm-ci.yml` stub
    calling wads’s reusable NPM workflow. Off by default.
  * **npm_subdir** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Subdirectory holding the JS/TS package (default `js`).
  * **npm_package_name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – npm package name (defaults to the project name).
  * **npm_version** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – initial npm package version (default `0.0.1`).
  * **npm_package_manager** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – package manager for the JS/TS package —
    `"npm"` (default) or `"pnpm"`. pnpm consumers should also declare a
    `"packageManager": "pnpm@x.y.z"` field in package.json.
  * **create_tests** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – If True, scaffold a `tests/` folder (`__init__.py`,
    a `test_<name>.py` smoke test, and a `tests/util.py` data accessor).
    Off by default so default output is unchanged.
  * **configs** – Extra configurations
* **Returns:**

### wads.populate.populate_proj_from_url(url, proj_rootdir=None, description=None, license='mit', \*\*kwargs)

git clone a repository and set the resulting folder up for packaging.

### wads.populate.update_pack_and_setup_py(target_pkg_dir, copy_files=('setup.py', 'wads/data/MANIFEST.in'))

Just copy over setup.py and pack.py (moving the original to be prefixed by ‘_’

### wads.populate.write_pyproject_configs(pkg_dir, configs)

Write pyproject.toml file from template and configs.

* **Parameters:**
  * **pkg_dir** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Path to package directory
  * **configs** ([`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)) – Dictionary of configuration values


# _autosummary/wads.profiles.html.md

# wads.profiles

Declarative generation *profiles* and *overlays* built on the engine.

A *profile* is a named declarative description of a project setup expressed as a
list of [`wads.templating.Artifact`](_autosummary/wads.templating.html.md#wads.templating.Artifact). The default profile (`python-lib`)
is produced by [`wads.populate`](_autosummary/wads.populate.html.md#module-wads.populate).

This module hosts two opt-in overlays added *on top of* an existing project:

- The **frontend profile registry** (issue #39): a small registry of
  language/toolchain profiles – `js` (the original #32 overlay, the
  back-compat anchor), `ts` (single-package TypeScript), and `ts-monorepo`
  (pnpm workspaces + turbo). A project may declare several frontend components
  (e.g. `js` *and* `ts`); each lives in its own subdir with its own
  path-filtered workflow, so they never collide. Selected via
  `populate --frontend <profile>[,<profile>]` (`--with-npm` stays as an
  alias for `--frontend js`). Profiles are extensible through
  [`register_frontend_profile()`](_autosummary/wads.profiles.html.md#wads.profiles.register_frontend_profile).
- The **tests-folder overlay** (issue #4): scaffolds a `tests/` package.

Nothing here runs by default; the Python `populate` path and the
`name/name/` root layout are untouched.

### Module Attributes

| [`FRONTEND_PROFILES`](_autosummary/wads.profiles.html.md#wads.profiles.FRONTEND_PROFILES)   | The frontend profile registry.   |
|----------------------------------------------------------------------|----------------------------------|

### Functions

| [`apply_frontend`](_autosummary/wads.profiles.html.md#wads.profiles.apply_frontend)(pkg_dir, \*[, profile, ...])      | Apply one frontend profile component to an existing project directory.        |
|---------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------|
| [`apply_npm_overlay`](_autosummary/wads.profiles.html.md#wads.profiles.apply_npm_overlay)(pkg_dir, \*, project_name)     | Deprecated alias for `apply_frontend(profile="js", ...)` (issue #32 surface). |
| [`apply_tests_overlay`](_autosummary/wads.profiles.html.md#wads.profiles.apply_tests_overlay)(pkg_dir, \*, project_name)   | Scaffold a `tests/` folder in an existing project (issue #4).                 |
| [`frontend_context`](_autosummary/wads.profiles.html.md#wads.profiles.frontend_context)(\*, profile_name, project_name) | Build the render context shared by all frontend-profile templates.            |
| [`get_frontend_profile`](_autosummary/wads.profiles.html.md#wads.profiles.get_frontend_profile)(name)                       | Look up a registered profile, with an informative error if unknown.           |
| [`make_tests_overlay_artifacts`](_autosummary/wads.profiles.html.md#wads.profiles.make_tests_overlay_artifacts)(project_name, \*)   | Artifacts that scaffold a `tests/` folder for a project.                      |
| [`npm_overlay_artifacts`](_autosummary/wads.profiles.html.md#wads.profiles.npm_overlay_artifacts)([npm_subdir])              | Deprecated: the `js` overlay artifacts.                                       |
| [`npm_overlay_context`](_autosummary/wads.profiles.html.md#wads.profiles.npm_overlay_context)(\*, project_name[, ...])     | Deprecated: render context for the `js` overlay.                              |
| [`register_frontend_profile`](_autosummary/wads.profiles.html.md#wads.profiles.register_frontend_profile)(profile)               | Register (or override) a frontend profile by name.                            |

### Classes

| [`FrontendProfile`](_autosummary/wads.profiles.html.md#wads.profiles.FrontendProfile)(name, default_subdir, ...[, ...])   | A declarative frontend language/toolchain profile.   |
|------------------------------------------------------------------------------------------------------|------------------------------------------------------|

### wads.profiles.FRONTEND_PROFILES *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)* *= {'js': FrontendProfile(name='js', default_subdir='js', default_package_manager='npm', artifacts=<function \_js_artifacts>, description='JavaScript single package (the original #32 overlay).'), 'ts': FrontendProfile(name='ts', default_subdir='ts', default_package_manager='npm', artifacts=<function \_ts_artifacts>, description='TypeScript single package (tsconfig + tsup build + vitest).'), 'ts-monorepo': FrontendProfile(name='ts-monorepo', default_subdir='ts', default_package_manager='pnpm', artifacts=<function \_ts_monorepo_artifacts>, description='TypeScript monorepo (pnpm workspaces + turbo).')}*

The frontend profile registry. Extend it via [`register_frontend_profile()`](_autosummary/wads.profiles.html.md#wads.profiles.register_frontend_profile).

### *class* wads.profiles.FrontendProfile(name, default_subdir, default_package_manager, artifacts, description='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A declarative frontend language/toolchain profile.

* **Parameters:**
  * **name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – registry key (e.g. `"js"`, `"ts"`, `"ts-monorepo"`).
  * **default_subdir** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – subdir the component lives in when unspecified.
  * **default_package_manager** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – `"npm"` or `"pnpm"`.
  * **artifacts** ([`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`Mapping`](https://docs.python.org/3/library/typing.html#typing.Mapping)], [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)]) – `context -> list[Artifact]` builder for this profile.
  * **description** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – one-line human summary.

### wads.profiles.apply_frontend(pkg_dir, , profile='js', project_name, description='', license=None, subdir=None, package_name=None, version='0.0.1', package_manager=None, workspace_glob='packages/\*', example_package_name=None, overwrite=(), on_add=None, on_skip=None)

Apply one frontend profile component to an existing project directory.

`subdir` and `package_manager` default to the selected profile’s
defaults. Returns the [`wads.templating.GenerationResult`](_autosummary/wads.templating.html.md#wads.templating.GenerationResult).

* **Return type:**
  [`GenerationResult`](_autosummary/wads.templating.html.md#wads.templating.GenerationResult)

```pycon
>>> import tempfile
>>> d = tempfile.mkdtemp()
>>> res = apply_frontend(d, profile="ts", project_name="widget")
>>> sorted(res.added)
['.github/workflows/npm-ci-ts.yml', 'ts/package.json', 'ts/src/index.test.ts',
 'ts/src/index.ts', 'ts/tsconfig.json']
```

### wads.profiles.apply_npm_overlay(pkg_dir, , project_name, description='', license=None, npm_subdir='js', npm_package_name=None, npm_version='0.0.1', npm_package_manager='npm', overwrite=(), on_add=None, on_skip=None)

Deprecated alias for `apply_frontend(profile="js", ...)` (issue #32 surface).

Kept so existing callers and tests keep working; new code should call
[`apply_frontend()`](_autosummary/wads.profiles.html.md#wads.profiles.apply_frontend).

### wads.profiles.apply_tests_overlay(pkg_dir, , project_name, with_data_util=True, overwrite=(), on_add=None, on_skip=None)

Scaffold a `tests/` folder in an existing project (issue #4).

### wads.profiles.frontend_context(, profile_name, project_name, description='', license=None, subdir, package_name=None, version='0.0.1', package_manager='npm', workspace_glob='packages/\*', example_package_name=None)

Build the render context shared by all frontend-profile templates.

The returned mapping is a superset: it carries the keys every profile’s
templates may reference (single-package and monorepo). Unused keys are
harmless under Jinja `StrictUndefined` because that only errors on keys a
template *references* yet are missing.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### wads.profiles.get_frontend_profile(name)

Look up a registered profile, with an informative error if unknown.

* **Return type:**
  [`FrontendProfile`](_autosummary/wads.profiles.html.md#wads.profiles.FrontendProfile)

### wads.profiles.make_tests_overlay_artifacts(project_name, , with_data_util=True)

Artifacts that scaffold a `tests/` folder for a project.

Writes `tests/__init__.py`, `tests/test_<project_name>.py` (a smoke
test), and optionally `tests/util.py` (a `tests/data` accessor, per #4).
The smoke-test filename embeds the package name, so it is built here where
the name is known.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)

### wads.profiles.npm_overlay_artifacts(npm_subdir='js')

Deprecated: the `js` overlay artifacts. Use the `js` frontend profile.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)

### wads.profiles.npm_overlay_context(, project_name, description='', license=None, npm_subdir='js', npm_package_name=None, npm_version='0.0.1', npm_package_manager='npm')

Deprecated: render context for the `js` overlay. Use [`apply_frontend()`](_autosummary/wads.profiles.html.md#wads.profiles.apply_frontend).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### wads.profiles.register_frontend_profile(profile)

Register (or override) a frontend profile by name. Returns the profile.

* **Return type:**
  [`FrontendProfile`](_autosummary/wads.profiles.html.md#wads.profiles.FrontendProfile)


# _autosummary/wads.project_setup.html.md

# wads.project_setup

Project setup utilities: name checking, GitHub operations, and orchestration.

This module provides the building blocks for AI-assisted project creation.
Each function is independently useful — they can be called from Python, CLI, or
a Claude skill.

Key capabilities:

- Check package name availability on PyPI and GitHub
- Manage name candidate files (pools of potential names)
- Create GitHub repositories via the gh CLI
- Orchestrate full project creation (repo → populate → commit/push)

### Functions

| [`check_name_availability`](_autosummary/wads.project_setup.html.md#wads.project_setup.check_name_availability)(name, \*[, org, npm, ...])   | Check a package name's validity and availability on PyPI (and optionally npm, GitHub).          |
|-------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------|
| [`check_names`](_autosummary/wads.project_setup.html.md#wads.project_setup.check_names)(names, \*[, org])                        | Check multiple names for availability (kwargs: `npm`, `github`, see `check_name_availability`). |
| [`create_github_repo`](_autosummary/wads.project_setup.html.md#wads.project_setup.create_github_repo)(name, \*[, org, ...])             | Create a GitHub repository via the `gh` CLI.                                                    |
| [`create_misc_docs`](_autosummary/wads.project_setup.html.md#wads.project_setup.create_misc_docs)(pkg_dir, \*[, sections])            | Create misc/docs/ directory with template markdown files.                                       |
| [`detect_github_username`](_autosummary/wads.project_setup.html.md#wads.project_setup.detect_github_username)()                             | Detect the current GitHub username.                                                             |
| [`github_repo_url`](_autosummary/wads.project_setup.html.md#wads.project_setup.github_repo_url)(name, \*[, org])                     | Return the GitHub repository URL for org/name.                                                  |
| [`is_available_on_github`](_autosummary/wads.project_setup.html.md#wads.project_setup.is_available_on_github)(name, \*[, org])              | Check if a repository name is available on GitHub.                                              |
| [`is_available_on_npm`](_autosummary/wads.project_setup.html.md#wads.project_setup.is_available_on_npm)(name, \*[, timeout])             | Check if a package name is unclaimed on the npm registry (HTTP 404 = free).                     |
| [`is_available_on_pypi`](_autosummary/wads.project_setup.html.md#wads.project_setup.is_available_on_pypi)(name)                           | Check if a package name is unclaimed on PyPI.                                                   |
| [`list_name_candidate_files`](_autosummary/wads.project_setup.html.md#wads.project_setup.list_name_candidate_files)()                          | List all name candidate files in the name_candidates directory.                                 |
| [`load_name_candidates`](_autosummary/wads.project_setup.html.md#wads.project_setup.load_name_candidates)([filepath])                     | Load name candidates from a file or all files in the name_candidates directory.                 |
| [`pypi_project_url`](_autosummary/wads.project_setup.html.md#wads.project_setup.pypi_project_url)(name)                               | Return the PyPI project page URL for a package name.                                            |
| [`repo_exists`](_autosummary/wads.project_setup.html.md#wads.project_setup.repo_exists)(name, \*[, org])                         | Check if a GitHub repository exists at org/name.                                                |
| [`setup_opsward_for_project`](_autosummary/wads.project_setup.html.md#wads.project_setup.setup_opsward_for_project)(pkg_dir)                   | Set up AI agent configuration using opsward, if available.                                      |
| [`setup_project`](_autosummary/wads.project_setup.html.md#wads.project_setup.setup_project)(name, \*[, description, org, ...])     | Orchestrate full project creation.                                                              |

### wads.project_setup.check_name_availability(name, , org=None, npm=False, github=False)

Check a package name’s validity and availability on PyPI (and optionally npm, GitHub).

PyPI is always checked. Pass `npm=True` when the project also publishes to npm.
GitHub is checked only with `github=True` (explicit user request): GitHub
calls are rate limited, and PyPI/npm are what decide a package name.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

Returns a dict with keys:
: name, valid_pep508, pypi_available, pypi_url,
  npm_available, npm_url, github_available, github_url

(keys of checks that were not run are None)

```pycon
>>> result = check_name_availability("wads")
>>> result["valid_pep508"]
True
```

### wads.project_setup.check_names(names, , org=None, \*\*kwargs)

Check multiple names for availability (kwargs: `npm`, `github`, see
`check_name_availability`). Returns a list of result dicts.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]

### wads.project_setup.create_github_repo(name, , org=None, description='', public=True, clone=True, clone_dir=None)

Create a GitHub repository via the `gh` CLI.

* **Parameters:**
  * **name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Repository name.
  * **org** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – GitHub org or username. Detected if not provided.
  * **description** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Repository description.
  * **public** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – If True, create a public repo. If False, private.
  * **clone** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – If True, clone the repo after creation.
  * **clone_dir** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Directory to clone into. Defaults to current dir.
* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)
* **Returns:**
  The local path to the cloned/created repository.
* **Raises:**
  * [**EnvironmentError**](https://docs.python.org/3/builtins/exceptions.html#EnvironmentError) – If `gh` CLI is not available.
  * [**CalledProcessError**](https://docs.python.org/3/library/subprocess.html#subprocess.CalledProcessError) – If repo creation fails.

### wads.project_setup.create_misc_docs(pkg_dir, , sections=None)

Create misc/docs/ directory with template markdown files.

* **Parameters:**
  * **pkg_dir** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Project root directory.
  * **sections** ([`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)] | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – List of section names to create. Defaults to
    [“research”, “design”, “roadmap”].
* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]
* **Returns:**
  List of created file paths.

### wads.project_setup.detect_github_username()

Detect the current GitHub username.

Tries `gh auth status` first, then falls back to `git config user.name`.
Returns None if detection fails.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### wads.project_setup.github_repo_url(name, , org=None)

Return the GitHub repository URL for org/name.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### wads.project_setup.is_available_on_github(name, , org=None)

Check if a repository name is available on GitHub.

Requires the `gh` CLI to be installed and authenticated.
Returns True if no repo exists at org/name.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

### wads.project_setup.is_available_on_npm(name, , timeout=10)

Check if a package name is unclaimed on the npm registry (HTTP 404 = free).

Returns None when the registry could not give a definite answer.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

```pycon
>>> is_available_on_npm("zzz-nonexistent-pkg-12345")
True
```

### wads.project_setup.is_available_on_pypi(name)

Check if a package name is unclaimed on PyPI.

Returns True if the name is available (no package exists with that name).

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

```pycon
>>> is_available_on_pypi("wads")
False
>>> is_available_on_pypi("zzz_nonexistent_pkg_12345")
True
```

### wads.project_setup.list_name_candidate_files()

List all name candidate files in the name_candidates directory.

Name candidate files are plain text files (one name per line).
Lines starting with # are comments; blank lines are ignored.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)]

### wads.project_setup.load_name_candidates(filepath=None)

Load name candidates from a file or all files in the name_candidates directory.

* **Parameters:**
  **filepath** ([`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Specific file to load from. If None, loads from all files
  in the name_candidates directory.
* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]
* **Returns:**
  List of candidate names (deduplicated, preserving order).

### wads.project_setup.pypi_project_url(name)

Return the PyPI project page URL for a package name.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### wads.project_setup.repo_exists(name, , org=None)

Check if a GitHub repository exists at org/name.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

### wads.project_setup.setup_opsward_for_project(pkg_dir)

Set up AI agent configuration using opsward, if available.

Returns True if opsward ran successfully, False if not available.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

### wads.project_setup.setup_project(name, , description='', org=None, author=None, license='mit', root_url=None, proj_rootdir=None, create_repo=True, populate=True, create_devdocs=False, setup_opsward=False, verbose=True)

Orchestrate full project creation.

Steps (each independently skippable via keyword args):

1. Validate name
2. Create GitHub repo (if create_repo)
3. Populate project files (if populate)
4. Create misc/docs/ (if create_devdocs)
5. Run opsward generate (if setup_opsward and opsward is available)

Returns a summary dict with keys: path, steps_completed, warnings.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)


# _autosummary/wads.repo_audit.html.md

# wads.repo_audit

Read-only health audit of a (wads-managed) Python repo.

Checks every health dimension a repo doctor cares about — legacy packaging
leftovers, pyproject metadata shape, CI workflow generation (uv stub / inline
uv / 2025 / legacy / none), stale test paths, module docstring presence,
GitHub metadata drift (description / homepage / topics vs pyproject), Pages
status, latest CI run conclusion, and PyPI-vs-pyproject version sync — and
prints a prioritized report grouped HIGH / MEDIUM / LOW, each finding tagged
with the specialist skill (or CLI) that owns the fix.

This module is the **single source of truth** for the ecosystem’s “what is a
modern wads repo” classification. Import it:

```default
from wads.repo_audit import audit_repo
report = audit_repo("/path/to/repo", network=False)   # dict: facts + findings
```

or run it as a script / module:

```default
python -m wads.repo_audit [REPO_DIR] [--json] [--no-network]
```

REPO_DIR defaults to the current directory. `--json` emits the full
machine-readable report. `--no-network` skips everything that leaves the
machine (gh calls and the PyPI lookup); it also degrades gracefully when
`gh` is absent or offline.

Stdlib only. Strictly read-only: never modifies the target repo or anything
else. The `wads-repo-doctor` skill ships a thin shim (`scripts/repo_audit.py`)
that defers to this module.

### Functions

| `audit_ci`(repo, rep)                                                                       |                                                                     |
|---------------------------------------------------------------------------------------------|---------------------------------------------------------------------|
| `audit_files`(repo, rep)                                                                    |                                                                     |
| `audit_git`(repo, rep)                                                                      |                                                                     |
| [`audit_github`](_autosummary/wads.repo_audit.html.md#wads.repo_audit.audit_github)(repo_slug, rep, project, ...) | gh-based checks; every step degrades silently when gh/network fail. |
| `audit_package`(repo, rep, project_name)                                                    |                                                                     |
| `audit_pypi`(rep, project)                                                                  |                                                                     |
| `audit_pyproject`(repo, rep)                                                                |                                                                     |
| [`audit_repo`](_autosummary/wads.repo_audit.html.md#wads.repo_audit.audit_repo)([repo, network])                | Audit a repo and return the machine-readable report dict.           |
| `audit_skills`(repo, rep, project_name)                                                     |                                                                     |
| [`classify_workflow`](_autosummary/wads.repo_audit.html.md#wads.repo_audit.classify_workflow)(text)                    | Classify one workflow file's text into a wads CI generation.        |
| `find_package_dir`(repo, project_name)                                                      |                                                                     |
| `main`([argv])                                                                              |                                                                     |
| `read_pyproject`(repo)                                                                      |                                                                     |
| `render_text`(rep)                                                                          |                                                                     |

### Classes

| [`Report`](_autosummary/wads.repo_audit.html.md#wads.repo_audit.Report)(repo)   | Accumulates facts and findings; renders text or JSON.   |
|-----------------------------------------------------------------|---------------------------------------------------------|

### *class* wads.repo_audit.Report(repo)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Accumulates facts and findings; renders text or JSON.

### wads.repo_audit.audit_github(repo_slug, rep, project, docs_enabled)

gh-based checks; every step degrades silently when gh/network fail.

### wads.repo_audit.audit_repo(repo='.', , network=True)

Audit a repo and return the machine-readable report dict.

This is the importable entry point (the SSOT classifier). It returns the
same structure as `python -m wads.repo_audit <repo> --json`:

```default
{"repo": ..., "summary": {HIGH/MEDIUM/LOW counts},
 "findings": [...], "facts": {...}}
```

`network=False` skips everything that leaves the machine (`gh` + PyPI),
matching the `--no-network` CLI flag.

### wads.repo_audit.classify_workflow(text)

Classify one workflow file’s text into a wads CI generation.


# _autosummary/wads.scripts.build_dist.html.md

# wads.scripts.build_dist

Build Python Distribution Packages

This script builds Python distribution packages (sdist and/or wheel) using
modern PEP 517 build tools.

Usage:

```default
python -m wads.scripts.build_dist [options]
```

* **param –output-dir PATH    Output directory for built distributions (default:**
  dist)
* **param –sdist             Build source distribution (default:**
  true)
* **param –wheel             Build wheel distribution (default:**
  true)
* **param –no-sdist          Skip building source distribution:**
* **param –no-wheel          Skip building wheel distribution:**

### Functions

| [`build_distributions`](_autosummary/wads.scripts.build_dist.html.md#wads.scripts.build_dist.build_distributions)([output_dir, ...])   | Build distribution packages.   |
|-------------------------------------------------------------------------------------------|--------------------------------|
| [`main`](_autosummary/wads.scripts.build_dist.html.md#wads.scripts.build_dist.main)()                                   | Main entry point.              |

### wads.scripts.build_dist.build_distributions(output_dir='dist', build_sdist=True, build_wheel=True)

Build distribution packages.

* **Parameters:**
  * **output_dir** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Output directory for distributions
  * **build_sdist** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Whether to build source distribution
  * **build_wheel** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Whether to build wheel distribution
* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int)
* **Returns:**
  Exit code (0 for success, 1 for failure)

### wads.scripts.build_dist.main()

Main entry point.


# _autosummary/wads.scripts.export_ci_env.html.md

# wads.scripts.export_ci_env

Export the pyproject-declared CI environment to `$GITHUB_ENV`.

This is the run-time half of wads’ two-layer secret model (see
[`wads.ci_secrets`](_autosummary/wads.ci_secrets.html.md#module-wads.ci_secrets)). The reusable workflow receives the caller’s secrets
either as named pass-throughs (the default stub) or as one double-encoded JSON
blob under `WADS_CI_SECRETS_JSON` (opt-in JSON stubs); this script decides which
values actually become job environment variables, driven entirely by
`[tool.wads.ci.env]` in the consumer’s `pyproject.toml` (read via the
`read-ci-config` action):

* `defaults`  — literal `KEY=value` pairs, always written.
* `required_envvars` — must resolve non-empty, else CI **fails** with a
  precise message.
* `test_envvars` — exported if set; a **warning** is emitted if missing.
* `extra_envvars` — exported if set; silent if missing.
* `secret_aliases` — map `ENV_VAR -> SECRET_NAME` for the (rare) case where
  the env var the code reads differs from the GitHub secret name.

Each declared name resolves against **secrets first** (named ones merged with
the transport blob) and the caller’s **repository variables second** (the
`vars` context resolves to the caller’s repo in a reusable workflow) — so a
non-sensitive value like a test verbosity level can live in a repo variable
instead of being mis-classified as a secret.

Secret *values* are never printed. Values extracted from the transport blob
are not auto-masked by GitHub (only the blob as a whole is), so every
secret-sourced value is re-registered with `::add-mask::` (per line, the
documented form for multiline values) before being written to the job env.

### Functions

| [`export_ci_env`](_autosummary/wads.scripts.export_ci_env.html.md#wads.scripts.export_ci_env.export_ci_env)(\*[, required, test, extra, ...])   | Compute the env assignments to write, plus any missing-required errors.             |
|----------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------|
| [`is_publish_credential`](_autosummary/wads.scripts.export_ci_env.html.md#wads.scripts.export_ci_env.is_publish_credential)(env_var[, secret])          | True when `env_var` (backed by `secret`) carries a PyPI publish credential.         |
| [`main`](_autosummary/wads.scripts.export_ci_env.html.md#wads.scripts.export_ci_env.main)()                                            | Read inputs from env, write assignments to `$GITHUB_ENV`, fail if required missing. |
| [`merge_transported_secrets`](_autosummary/wads.scripts.export_ci_env.html.md#wads.scripts.export_ci_env.merge_transported_secrets)(secrets)                | Flatten the secrets mapping, expanding the `WADS_CI_SECRETS_JSON` blob.             |

### Classes

| [`ExportPlan`](_autosummary/wads.scripts.export_ci_env.html.md#wads.scripts.export_ci_env.ExportPlan)(assignments, exported, ...)   | What [`export_ci_env()`](_autosummary/wads.scripts.export_ci_env.html.md#wads.scripts.export_ci_env.export_ci_env) decided, ready for [`main()`](_autosummary/wads.scripts.export_ci_env.html.md#wads.scripts.export_ci_env.main) to apply.   |
|-------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|

### *class* wads.scripts.export_ci_env.ExportPlan(assignments, exported, missing_required, missing_test, mask_values)

Bases: [`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)

What [`export_ci_env()`](_autosummary/wads.scripts.export_ci_env.html.md#wads.scripts.export_ci_env.export_ci_env) decided, ready for [`main()`](_autosummary/wads.scripts.export_ci_env.html.md#wads.scripts.export_ci_env.main) to apply.

#### assignments *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)*

Alias for field number 0

#### exported *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)*

Alias for field number 1

#### mask_values *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)*

Alias for field number 4

#### missing_required *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)*

Alias for field number 2

#### missing_test *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)*

Alias for field number 3

### wads.scripts.export_ci_env.export_ci_env(\*, required=(), test=(), extra=(), defaults=None, aliases=None, secrets=None, vars_=None, withhold_publish=True, warn=<function <lambda>>)

Compute the env assignments to write, plus any missing-required errors.

Pure function (no I/O) so it can be unit-tested without GitHub. Each
declared name resolves secrets-first, then falls back to the caller’s
repository variables (`vars_`). Secret-sourced values are collected in
`mask_values` for re-masking; variable- and default-sourced values are
not masked (they are not sensitive by definition).

```pycon
>>> plan = export_ci_env(
...     required=["OPENAI_API_KEY"],
...     test=["TAVILY_API_KEY"],
...     extra=["UNSET_THING", "COSMO_TEST_LEVEL"],
...     defaults={"LOG_LEVEL": "DEBUG"},
...     secrets={"OPENAI_API_KEY": "sk-xxx", "TAVILY_API_KEY": ""},
...     vars_={"COSMO_TEST_LEVEL": "3"},
... )
>>> plan.exported
['LOG_LEVEL', 'OPENAI_API_KEY', 'COSMO_TEST_LEVEL']
>>> plan.missing_required
[]
>>> plan.missing_test
['TAVILY_API_KEY']
>>> plan.mask_values  # only the secret-sourced value
['sk-xxx']
```

A required var with no backing secret *or* variable is reported (caller
should fail CI):

```pycon
>>> export_ci_env(required=["OPENAI_API_KEY"], secrets={}).missing_required
[('OPENAI_API_KEY', 'OPENAI_API_KEY')]
```

Aliases let an env var read a differently-named secret:

```pycon
>>> plan = export_ci_env(
...     test=["HF_TOKEN"],
...     aliases={"HF_TOKEN": "HF_WRITE_TOKEN"},
...     secrets={"HF_WRITE_TOKEN": "hf_xxx"})
>>> plan.exported
['HF_TOKEN']
>>> plan.assignments
['HF_TOKEN=hf_xxx']
```

A publish credential (see [`is_publish_credential()`](_autosummary/wads.scripts.export_ci_env.html.md#wads.scripts.export_ci_env.is_publish_credential)) is never
exported, under any name, and never fails the job, unless
`withhold_publish=False` (the publish job). The token must not reach a
job that runs tests (issue #110):

```pycon
>>> plan = export_ci_env(
...     required=["PYPI_PASSWORD"],
...     extra=["UV_PUBLISH_TOKEN"],
...     aliases={"UV_PUBLISH_TOKEN": "PYPI_PASSWORD"},
...     secrets={"PYPI_PASSWORD": "pypi-xxx"},
...     warn=lambda msg: None)
>>> plan.exported, plan.missing_required, plan.mask_values
([], [], [])
```

### wads.scripts.export_ci_env.is_publish_credential(env_var, secret=None)

True when `env_var` (backed by `secret`) carries a PyPI publish credential.

Matches the publish env-var names tools read, the wads publish secrets, and
any other PyPI-ish credential name, case-insensitively (GitHub secret names
are case-insensitive).

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

```pycon
>>> is_publish_credential("UV_PUBLISH_TOKEN", "PYPI_PASSWORD")
True
>>> is_publish_credential("TWINE_PASSWORD", "WHATEVER")
True
>>> is_publish_credential("MY_TOKEN", "pypi_api_token")
True
>>> is_publish_credential("PYPI_USERNAME")
False
>>> is_publish_credential("OPENAI_API_KEY")
False
```

### wads.scripts.export_ci_env.main()

Read inputs from env, write assignments to `$GITHUB_ENV`, fail if required missing.

* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int)

### wads.scripts.export_ci_env.merge_transported_secrets(secrets)

Flatten the secrets mapping, expanding the `WADS_CI_SECRETS_JSON` blob.

The blob is the caller’s whole secrets context, serialized by the stub with
`toJSON(toJSON(secrets))` (double-encoded => single-line). A
single-encoded blob is also accepted. Named entries win over blob entries
(they are the same caller values anyway), and non-exportable keys (the
blob itself, `github_token`) are dropped.

```pycon
>>> merged = merge_transported_secrets({
...     "WADS_CI_SECRETS_JSON": '"{\\n  \\"MY_KEY\\": \\"v1\\",\\n  \\"github_token\\": \\"t\\"\\n}"',
...     "OPENAI_API_KEY": "sk-named",
... })
>>> sorted(merged)
['MY_KEY', 'OPENAI_API_KEY']
>>> merged["MY_KEY"]
'v1'
```

Malformed blobs are ignored rather than fatal (the named layer still
works, and required-validation reports any name that failed to resolve):

```pycon
>>> merge_transported_secrets({"WADS_CI_SECRETS_JSON": "not json", "A": "x"})
{'A': 'x'}
```


# _autosummary/wads.scripts.html.md

# wads.scripts

CI and automation scripts for wads.

These scripts extract complex logic from GitHub Actions workflow files,
making them easier to test, maintain, and reuse.

Available scripts:

> - build_dist: Build Python distribution packages
> - install_deps: Install Python dependencies
> - read_ci_config: Read CI configuration from pyproject.toml
> - set_env_vars: Set environment variables from GitHub Secrets
> - validate_ci_env: Validate CI environment variables

### Modules

| [`build_dist`](_autosummary/wads.scripts.build_dist.html.md#module-wads.scripts.build_dist)           | Build Python Distribution Packages                             |
|------------------------------------------------------------------------------------------------------|----------------------------------------------------------------|
| [`install_deps`](_autosummary/wads.scripts.install_deps.html.md#module-wads.scripts.install_deps)       | Install Dependencies                                           |
| [`read_ci_config`](_autosummary/wads.scripts.read_ci_config.html.md#module-wads.scripts.read_ci_config)   | Read CI Configuration and Export to GitHub Actions             |
| [`set_env_vars`](_autosummary/wads.scripts.set_env_vars.html.md#module-wads.scripts.set_env_vars)       | Set Environment Variables from GitHub Secrets                  |
| [`validate_ci_env`](_autosummary/wads.scripts.validate_ci_env.html.md#module-wads.scripts.validate_ci_env) | CI Environment Validation Script                               |
| [`export_ci_env`](_autosummary/wads.scripts.export_ci_env.html.md#module-wads.scripts.export_ci_env)     | Export the pyproject-declared CI environment to `$GITHUB_ENV`. |


# _autosummary/wads.scripts.install_deps.html.md

# wads.scripts.install_deps

Install Dependencies

This script installs Python dependencies from various sources including
pyproject.toml, requirements.txt, and direct package names.

Usage:

```default
python -m wads.scripts.install_deps [options]
```

* **param –pypi-packages PKG1 PKG2…  Python packages to install:**
* **param –dependency-files FILE1:**
* **param FILE2:**
* **param …  Dependency files to install from:**
* **param –extras EXTRA1:**
* **param EXTRA2:**
* **param …  Extras to install from pyproject.toml:**

### Functions

| [`install_from_dependency_files`](_autosummary/wads.scripts.install_deps.html.md#wads.scripts.install_deps.install_from_dependency_files)(files[, extras])   | Install from dependency files.   |
|---------------------------------------------------------------------------------------------------|----------------------------------|
| [`install_pypi_packages`](_autosummary/wads.scripts.install_deps.html.md#wads.scripts.install_deps.install_pypi_packages)(packages)                  | Install packages from PyPI.      |
| [`main`](_autosummary/wads.scripts.install_deps.html.md#wads.scripts.install_deps.main)()                                           | Main entry point.                |

### wads.scripts.install_deps.install_from_dependency_files(files, extras=None)

Install from dependency files.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

### wads.scripts.install_deps.install_pypi_packages(packages)

Install packages from PyPI.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

### wads.scripts.install_deps.main()

Main entry point.


# _autosummary/wads.scripts.read_ci_config.html.md

# wads.scripts.read_ci_config

Read CI Configuration and Export to GitHub Actions

This script reads CI configuration from pyproject.toml and exports it as
GitHub Actions outputs and environment variables.

Usage:

```default
python -m wads.scripts.read_ci_config [path_to_pyproject]
```

Environment:
: GITHUB_OUTPUT - Path to GitHub Actions output file
  GITHUB_ENV - Path to GitHub Actions environment file
  GITHUB_STEP_SUMMARY - Path to GitHub Actions step summary file

### Functions

| [`main`](_autosummary/wads.scripts.read_ci_config.html.md#wads.scripts.read_ci_config.main)()                                      | Main entry point.                                   |
|----------------------------------------------------------------------------------------------|-----------------------------------------------------|
| [`read_and_export_ci_config`](_autosummary/wads.scripts.read_ci_config.html.md#wads.scripts.read_ci_config.read_and_export_ci_config)([pyproject_path]) | Read CI configuration and export to GitHub Actions. |

### wads.scripts.read_ci_config.main()

Main entry point.

### wads.scripts.read_ci_config.read_and_export_ci_config(pyproject_path='.')

Read CI configuration and export to GitHub Actions.

* **Parameters:**
  **pyproject_path** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)) – Path to pyproject.toml file or directory containing it
* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int)
* **Returns:**
  Exit code (0 for success, 1 for failure)


# _autosummary/wads.scripts.set_env_vars.html.md

# wads.scripts.set_env_vars

Set Environment Variables from GitHub Secrets

This script reads the CI configuration from pyproject.toml and sets environment
variables from GitHub Secrets, validating required variables.

Usage:

```default
python -m wads.scripts.set_env_vars [path_to_pyproject]
```

Environment:
: SECRETS_CONTEXT - JSON string of all GitHub Secrets
  GITHUB_ENV - Path to GitHub Actions environment file
  GITHUB_STEP_SUMMARY - Path to GitHub Actions step summary file

### Functions

| [`main`](_autosummary/wads.scripts.set_env_vars.html.md#wads.scripts.set_env_vars.main)()                                      | Main entry point.                                                 |
|----------------------------------------------------------------------------------------------|-------------------------------------------------------------------|
| [`set_environment_variables`](_autosummary/wads.scripts.set_env_vars.html.md#wads.scripts.set_env_vars.set_environment_variables)([pyproject_path]) | Set environment variables from GitHub Secrets based on CI config. |

### wads.scripts.set_env_vars.main()

Main entry point.

### wads.scripts.set_env_vars.set_environment_variables(pyproject_path='.')

Set environment variables from GitHub Secrets based on CI config.

* **Parameters:**
  **pyproject_path** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)) – Path to pyproject.toml file or directory containing it
* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int)
* **Returns:**
  Exit code (0 for success, 1 for failure)


# _autosummary/wads.scripts.validate_ci_env.html.md

# wads.scripts.validate_ci_env

CI Environment Validation Script

This script validates that all required environment variables are set
based on configuration in pyproject.toml [tool.wads.ci.env].

Usage:
: python -m wads.scripts.validate_ci_env

Exit codes:
: 0 - All required environment variables are set
  1 - One or more required environment variables are missing

### Functions

| [`main`](_autosummary/wads.scripts.validate_ci_env.html.md#wads.scripts.validate_ci_env.main)()                                    | Main entry point for CI environment validation.              |
|--------------------------------------------------------------------------------------------|--------------------------------------------------------------|
| [`validate_ci_environment`](_autosummary/wads.scripts.validate_ci_env.html.md#wads.scripts.validate_ci_env.validate_ci_environment)([pyproject_path]) | Validate that all required CI environment variables are set. |

### wads.scripts.validate_ci_env.main()

Main entry point for CI environment validation.

### wads.scripts.validate_ci_env.validate_ci_environment(pyproject_path='.')

Validate that all required CI environment variables are set.

* **Parameters:**
  **pyproject_path** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)) – Path to directory containing pyproject.toml
* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`bool`](https://docs.python.org/3/builtins/functions.html#bool), [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]
* **Returns:**
  Tuple of (success, missing_vars)


# _autosummary/wads.secrets_cli.html.md

# wads.secrets_cli

`wads-secrets` — manage the CI secrets/env vars of a wads-managed repo.

A secret becomes usable in CI through two layers (see [`wads.ci_secrets`](_autosummary/wads.ci_secrets.html.md#module-wads.ci_secrets)
for the model):

1. **pyproject** `[tool.wads.ci.env]` — declares the env var (and whether it
   is required), so the reusable workflow exports it into the job environment.
2. **transport** — the repo’s `ci.yml` stub passes secrets to the reusable
   workflow. Named-transport stubs (the default for new stubs) list each
   secret explicitly, and every listed name must be in the wads
   superset; `add` inserts the line. Stubs on the opt-in JSON transport pass
   the whole secrets context as one `WADS_CI_SECRETS_JSON` secret, so \*no
   per-secret stub edit is needed\* there.

`wads-secrets add` performs the needed edits in one step, and can also set
the secret’s value on GitHub via `gh` — so a single command takes a secret
from “not configured” to “available in CI”. For values that are \*\*not
sensitive\*\* (a test verbosity level, a feature flag), use `--variable` to
store them as a GitHub repository *variable* instead of a secret — declared
env vars fall back to repo variables automatically, and committed constants
can simply live in `[tool.wads.ci.env].defaults`.

Examples:

```default
wads-secrets add OPENAI_API_KEY                 # var == secret name
wads-secrets add HF_TOKEN HF_WRITE_TOKEN         # env var <- aliased secret
wads-secrets add DB_URL --kind required          # fail CI if unset
wads-secrets add COSMO_TEST_LEVEL --variable     # non-sensitive: repo var
wads-secrets add OPENAI_API_KEY --no-github      # edit files only
wads-secrets list                                # show configured env vars
```

Names are forced to `UPPER_SNAKE` and value-looking inputs are rejected, so
you can’t accidentally commit a secret *value* as a name.

### Functions

| [`add`](_autosummary/wads.secrets_cli.html.md#wads.secrets_cli.add)(var_name[, secret_name, repo, kind, ...])   | Configure a CI value: declare it in pyproject, transport it, set its value.   |
|--------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------|
| [`add_env_var_to_pyproject`](_autosummary/wads.secrets_cli.html.md#wads.secrets_cli.add_env_var_to_pyproject)(pyproject_path, ...)   | Add `var_name` to `[tool.wads.ci.env]`; record alias if names differ.         |
| [`add_secret_to_stub`](_autosummary/wads.secrets_cli.html.md#wads.secrets_cli.add_secret_to_stub)(ci_file, secret_name)        | Ensure the stub `ci.yml` passes `secret_name` to the reusable workflow.       |
| [`list_`](_autosummary/wads.secrets_cli.html.md#wads.secrets_cli.list_)([pyproject])                              | List the env vars configured in `[tool.wads.ci.env]`.                         |
| [`main`](_autosummary/wads.secrets_cli.html.md#wads.secrets_cli.main)([argv])                                    | `wads-secrets` CLI entry point (argparse-based).                              |
| [`set_github_secret`](_autosummary/wads.secrets_cli.html.md#wads.secrets_cli.set_github_secret)(repo, secret_name, value, \*) | Set a secret (or, with `variable=True`, a repo variable) via `gh`.            |
| [`stub_transport_mode`](_autosummary/wads.secrets_cli.html.md#wads.secrets_cli.stub_transport_mode)(ci_file)                    | Classify the repo's `ci.yml`: `'json'`, `'named'`, `'inline'`, or `None`.     |
| [`superset`](_autosummary/wads.secrets_cli.html.md#wads.secrets_cli.superset)()                                      | Print the wads secret superset (names a stub may pass).                       |

### wads.secrets_cli.add(var_name, secret_name=None, , repo=None, kind='extra', value=None, github=True, variable=False, pyproject='pyproject.toml', ci_file='.github/workflows/ci.yml')

Configure a CI value: declare it in pyproject, transport it, set its value.

* **Parameters:**
  * **var_name** – env-var name your code reads (forced to UPPER_SNAKE).
  * **secret_name** – GitHub secret backing it (default: same as var_name).
  * **repo** – `org/repo` to set the secret on (default: git origin here).
  * **kind** – `extra` (default), `test`, or `required`.
  * **value** – secret value to push to GitHub (default: `$VAR_NAME` in env).
  * **github** – also set the value on GitHub via `gh` (default True).
  * **variable** – store as a repository *variable* instead of a secret —
    for non-sensitive values (a test verbosity level, a flag). Variables
    need no transport at all: the reusable workflow reads the caller’s
    `vars` context directly and declared env vars fall back to it.

### wads.secrets_cli.add_env_var_to_pyproject(pyproject_path, var_name, secret_name, , kind='extra')

Add `var_name` to `[tool.wads.ci.env]`; record alias if names differ.

Returns `(changed, existing_bucket)`. If the var is already declared,
makes no change and returns `(False, <bucket>)`.

### wads.secrets_cli.add_secret_to_stub(ci_file, secret_name)

Ensure the stub `ci.yml` passes `secret_name` to the reusable workflow.

Returns `(changed, reason)`. `reason` explains no-ops (JSON transport
passes everything already, already present, not a stub, file missing).

### wads.secrets_cli.list_(pyproject='pyproject.toml')

List the env vars configured in `[tool.wads.ci.env]`.

### wads.secrets_cli.main(argv=None)

`wads-secrets` CLI entry point (argparse-based).

### wads.secrets_cli.set_github_secret(repo, secret_name, value, , variable=False)

Set a secret (or, with `variable=True`, a repo variable) via `gh`.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

### wads.secrets_cli.stub_transport_mode(ci_file)

Classify the repo’s `ci.yml`: `'json'`, `'named'`, `'inline'`, or `None`.

`'inline'` means a workflow that does not call the reusable uv-ci (it
reads repo secrets directly and has no transport layer — and no
repo-variable fallback either). `None` means no ci.yml at all.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### wads.secrets_cli.superset()

Print the wads secret superset (names a stub may pass).


# _autosummary/wads.setup_utils.html.md

# wads.setup_utils

Utilities for setting up packages based on pyproject.toml configuration.

This module provides tools for users to:

- Install Python dependencies with various options
- Install system dependencies based on OS and toml specs
- Validate environment variables
- Diagnose missing dependencies and provide instructions

### Functions

| [`check_environment_variables`](_autosummary/wads.setup_utils.html.md#wads.setup_utils.check_environment_variables)(pyproject_path)     | Check required environment variables from pyproject.toml.          |
|--------------------------------------------------------------------------------------------------|--------------------------------------------------------------------|
| [`check_system_dependency`](_autosummary/wads.setup_utils.html.md#wads.setup_utils.check_system_dependency)(dep_name, dep_ops, ...) | Check if a system dependency is installed using check commands.    |
| [`diagnose_setup`](_autosummary/wads.setup_utils.html.md#wads.setup_utils.diagnose_setup)(pyproject_path[, ...])           | Diagnose missing dependencies and configuration issues.            |
| [`get_current_platform`](_autosummary/wads.setup_utils.html.md#wads.setup_utils.get_current_platform)()                          | Get current platform identifier (linux, macos, windows).           |
| [`get_installed_pip_packages`](_autosummary/wads.setup_utils.html.md#wads.setup_utils.get_installed_pip_packages)()                    | Get set of installed pip package names.                            |
| [`install_python_dependencies`](_autosummary/wads.setup_utils.html.md#wads.setup_utils.install_python_dependencies)(pyproject_path)     | Install Python dependencies from pyproject.toml.                   |
| [`install_system_dependencies`](_autosummary/wads.setup_utils.html.md#wads.setup_utils.install_system_dependencies)(pyproject_path)     | Install system dependencies based on pyproject.toml configuration. |
| [`is_package_importable`](_autosummary/wads.setup_utils.html.md#wads.setup_utils.is_package_importable)(package_name)             | Check if a Python package can be imported.                         |
| [`print_diagnostic_report`](_autosummary/wads.setup_utils.html.md#wads.setup_utils.print_diagnostic_report)(result)                 | Print a formatted diagnostic report.                               |

### Classes

| [`DiagnosticResult`](_autosummary/wads.setup_utils.html.md#wads.setup_utils.DiagnosticResult)(missing_python_deps, ...)    | Result of dependency diagnostics.   |
|------------------------------------------------------------------------------------------------|-------------------------------------|
| [`InstallResult`](_autosummary/wads.setup_utils.html.md#wads.setup_utils.InstallResult)(success, package_name, message) | Result of an installation attempt.  |

### *class* wads.setup_utils.DiagnosticResult(missing_python_deps, missing_system_deps, missing_env_vars, warnings, recommendations)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Result of dependency diagnostics.

### *class* wads.setup_utils.InstallResult(success, package_name, message, command_executed=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Result of an installation attempt.

### wads.setup_utils.check_environment_variables(pyproject_path, verbose=True)

Check required environment variables from pyproject.toml.

* **Parameters:**
  * **pyproject_path** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)) – Path to pyproject.toml or directory containing it
  * **verbose** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Print warnings for missing variables
* **Return type:**
  [`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]
* **Returns:**
  Dict mapping variable names to their values (None if missing)

### wads.setup_utils.check_system_dependency(dep_name, dep_ops, platform, verbose=True)

Check if a system dependency is installed using check commands.

* **Parameters:**
  * **dep_name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Simplified dependency name
  * **dep_ops** ([`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict)) – Operational metadata for the dependency
  * **platform** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Platform identifier (linux, macos, windows)
  * **verbose** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Print check attempts
* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)
* **Returns:**
  True if dependency is installed (or no check command available)

### wads.setup_utils.diagnose_setup(pyproject_path, check_python=True, check_system=True, check_env=True, platform=None)

Diagnose missing dependencies and configuration issues.

* **Parameters:**
  * **pyproject_path** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)) – Path to pyproject.toml or directory containing it
  * **check_python** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Check Python dependencies
  * **check_system** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Check system dependencies
  * **check_env** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Check environment variables
  * **platform** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – Platform identifier (auto-detected if None)
* **Return type:**
  [`DiagnosticResult`](_autosummary/wads.setup_utils.html.md#wads.setup_utils.DiagnosticResult)
* **Returns:**
  DiagnosticResult with comprehensive analysis

### wads.setup_utils.get_current_platform()

Get current platform identifier (linux, macos, windows).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)
* **Returns:**
  Platform string compatible with wads configuration

### wads.setup_utils.get_installed_pip_packages()

Get set of installed pip package names.

* **Return type:**
  [`Set`](https://docs.python.org/3/library/typing.html#typing.Set)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]
* **Returns:**
  Set of installed package names (lowercase)

### wads.setup_utils.install_python_dependencies(pyproject_path, exclude=None, check_importable=True, upgrade=False, allow_downgrade=False, extras=None, dry_run=False, verbose=True)

Install Python dependencies from pyproject.toml.

* **Parameters:**
  * **pyproject_path** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)) – Path to pyproject.toml or directory containing it
  * **exclude** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`List`](https://docs.python.org/3/library/typing.html#typing.List)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]) – List of package names to exclude from installation
  * **check_importable** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Only install if package is not already importable
  * **upgrade** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Pass –upgrade flag to pip
  * **allow_downgrade** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Allow pip to downgrade packages (adds –force-reinstall)
  * **extras** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`List`](https://docs.python.org/3/library/typing.html#typing.List)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]) – List of extras to install (e.g., [‘dev’, ‘test’])
  * **dry_run** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – If True, only show what would be installed
  * **verbose** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Print detailed progress information
* **Return type:**
  [`List`](https://docs.python.org/3/library/typing.html#typing.List)[[`InstallResult`](_autosummary/wads.setup_utils.html.md#wads.setup_utils.InstallResult)]
* **Returns:**
  List of InstallResult objects

### wads.setup_utils.install_system_dependencies(pyproject_path, platform=None, check_first=True, dry_run=False, verbose=True, interactive=True)

Install system dependencies based on pyproject.toml configuration.

This is a wrapper around wads.install_system_deps module.

* **Parameters:**
  * **pyproject_path** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)) – Path to pyproject.toml or directory containing it
  * **platform** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – Platform identifier (auto-detected if None)
  * **check_first** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Use check commands to verify if already installed (ignored in dry_run)
  * **dry_run** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – If True, only show what would be installed
  * **verbose** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Print detailed progress information
  * **interactive** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Ask for confirmation before installing (not yet implemented)
* **Return type:**
  [`List`](https://docs.python.org/3/library/typing.html#typing.List)[[`InstallResult`](_autosummary/wads.setup_utils.html.md#wads.setup_utils.InstallResult)]
* **Returns:**
  List of InstallResult objects

### wads.setup_utils.is_package_importable(package_name)

Check if a Python package can be imported.

* **Parameters:**
  **package_name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Name of the package to check
* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)
* **Returns:**
  True if the package is importable

### wads.setup_utils.print_diagnostic_report(result)

Print a formatted diagnostic report.

* **Parameters:**
  **result** ([`DiagnosticResult`](_autosummary/wads.setup_utils.html.md#wads.setup_utils.DiagnosticResult)) – DiagnosticResult to display


# _autosummary/wads.templating.html.md

# wads.templating

Declarative, template-source-driven generation engine.

The core idea (issue #32, originally #3): a *template source* is any
`Mapping[str, str]` mapping a relative path to template *content*. Three
concrete kinds are supported, exactly matching the original spec:

- an **in-memory mapping** – a plain `dict` (or one loaded from JSON/YAML),
- a **local filesystem folder** – [`FilesystemTemplateSource`](_autosummary/wads.templating.html.md#wads.templating.FilesystemTemplateSource),
- a **remote folder** hosted on GitHub – [`GithubTemplateSource`](_autosummary/wads.templating.html.md#wads.templating.GithubTemplateSource).

Because a template source is just a `Mapping`, any key-value store (including
`dol`-style stores) can be used as one without wads depending on it.

Rendering is a thin layer over Jinja2 using **custom delimiters** so template
markup never collides with GitHub Actions `${{ ... }}` expressions or shell
`${...}`:

| Jinja2 default   | wads delimiter   |
|------------------|------------------|
| `{{ var }}`      | `<< var >>`      |
| `{% block %}`    | `<% block %>`    |
| `{# comment #}`  | `<# comment #>`  |

A small set of *render strategies* lets the engine reproduce historical wads
output exactly: `copy` (verbatim), `jinja` (Jinja2 with the delimiters
above), and `placeholder` (legacy `#TOKEN#` substitution kept for old CI
templates).

An [`Artifact`](_autosummary/wads.templating.html.md#wads.templating.Artifact) declaratively describes one generated file: its target
path, how its content is produced, and an optional condition. A *profile* is
just a list of artifacts; [`generate()`](_autosummary/wads.templating.html.md#wads.templating.generate) walks one and writes the files,
honoring an `overwrite` set and recording actions on an optional tracker.

```pycon
>>> source = {"greeting.txt": "Hello << name >>!"}
>>> render(source["greeting.txt"], {"name": "world"})
'Hello world!'
```

### Module Attributes

| [`JINJA_DELIMITERS`](_autosummary/wads.templating.html.md#wads.templating.JINJA_DELIMITERS)   | Jinja2 delimiters chosen to avoid colliding with <br/><br/>```<br/>``<br/>```<br/><br/>${{ .   |
|---------------------------------------------------------------------|------------------------------------------------------------------------------------------------|

### Functions

| [`apply_placeholders`](_autosummary/wads.templating.html.md#wads.templating.apply_placeholders)(content, mapping)         | Legacy `#TOKEN#` substitution used by the older CI templates.                              |
|-----------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------|
| [`generate`](_autosummary/wads.templating.html.md#wads.templating.generate)(target_dir, artifacts, context, \*) | Apply a list of [`Artifact`](_autosummary/wads.templating.html.md#wads.templating.Artifact) to `target_dir`. |
| [`make_jinja_env`](_autosummary/wads.templating.html.md#wads.templating.make_jinja_env)(\*\*overrides)                | Build a Jinja2 environment with wads's custom delimiters.                                  |
| [`render`](_autosummary/wads.templating.html.md#wads.templating.render)(content, context, \*[, env])          | Render `content` as a Jinja2 template with wads delimiters.                                |

### Classes

| [`Artifact`](_autosummary/wads.templating.html.md#wads.templating.Artifact)(target, content[, when, binary])      | One file to generate within a project.                                  |
|-------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------|
| [`FilesystemTemplateSource`](_autosummary/wads.templating.html.md#wads.templating.FilesystemTemplateSource)(root, \*[, encoding]) | A template source backed by a local folder.                             |
| [`GenerationResult`](_autosummary/wads.templating.html.md#wads.templating.GenerationResult)([added, skipped, errored])    | Outcome of [`generate()`](_autosummary/wads.templating.html.md#wads.templating.generate). |
| [`GithubTemplateSource`](_autosummary/wads.templating.html.md#wads.templating.GithubTemplateSource)(repo, \*[, ref, subdir])  | A template source backed by a folder in a GitHub repository.            |

### *class* wads.templating.Artifact(target, content, when=None, binary=False)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One file to generate within a project.

* **Parameters:**
  * **target** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – relative path of the file in the generated project.
  * **content** ([`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`Mapping`](https://docs.python.org/3/library/typing.html#typing.Mapping)], [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]) – a callable `context -> str | None` producing the file
    text (`None` to skip). Use the `from_*` constructors for the common
    `copy` / `jinja` / `placeholder` strategies.
  * **when** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`Mapping`](https://docs.python.org/3/library/typing.html#typing.Mapping)], [`bool`](https://docs.python.org/3/builtins/functions.html#bool)]]) – optional predicate `context -> bool` gating generation.
  * **binary** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – if True, `content` returns `bytes` and is written binary.

#### *classmethod* from_copy(target, source_key, source, \*\*kw)

Artifact that copies `source[source_key]` verbatim.

#### *classmethod* from_jinja(target, source_key, source, \*\*kw)

Artifact that renders `source[source_key]` with Jinja2.

#### *classmethod* from_placeholder(target, source_key, source, placeholders, \*\*kw)

Artifact that applies `#TOKEN#` substitution to a template.

### *class* wads.templating.FilesystemTemplateSource(root, , encoding='utf-8')

Bases: [`Mapping`](https://docs.python.org/3/library/typing.html#typing.Mapping)

A template source backed by a local folder.

Keys are POSIX-style relative paths of the files under `root`; values are
the (text) file contents, read lazily on access.

```pycon
>>> import tempfile, os
>>> d = tempfile.mkdtemp()
>>> _ = open(os.path.join(d, "a.txt"), "w").write("A")
>>> src = FilesystemTemplateSource(d)
>>> src["a.txt"]
'A'
>>> list(src)
['a.txt']
```

### *class* wads.templating.GenerationResult(added=<factory>, skipped=<factory>, errored=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Outcome of [`generate()`](_autosummary/wads.templating.html.md#wads.templating.generate).

### *class* wads.templating.GithubTemplateSource(repo, , ref='master', subdir='')

Bases: [`Mapping`](https://docs.python.org/3/library/typing.html#typing.Mapping)

A template source backed by a folder in a GitHub repository.

Files are fetched lazily from `raw.githubusercontent.com`. Uses the
standard library only (no `requests` dependency) so it stays usable in a
light install.

* **Parameters:**
  * **repo** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – `"owner/name"` slug.
  * **ref** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – branch, tag, or commit (default `"master"`).
  * **subdir** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – folder within the repo to treat as the template root.

Listing (`__iter__` / `__len__`) uses the GitHub trees API and is only
needed when enumerating; direct `source[key]` access does not list.

### wads.templating.JINJA_DELIMITERS *= {'block_end_string': '%>', 'block_start_string': '<%', 'comment_end_string': '#>', 'comment_start_string': '<#', 'variable_end_string': '>>', 'variable_start_string': '<<'}*

Jinja2 delimiters chosen to avoid colliding with `${{ ... }}` (GitHub
Actions) and `${...}` (shell).

### wads.templating.apply_placeholders(content, mapping)

Legacy `#TOKEN#` substitution used by the older CI templates.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> apply_placeholders("name=#NAME#", {"#NAME#": "wads"})
'name=wads'
```

### wads.templating.generate(target_dir, artifacts, context, , overwrite=(), on_add=None, on_skip=None, on_error=None)

Apply a list of [`Artifact`](_autosummary/wads.templating.html.md#wads.templating.Artifact) to `target_dir`.

An artifact is written when its target does not exist *or* its target is in
`overwrite`. Parent directories are created as needed. Each action is
reported through the optional `on_*` callbacks and accumulated in the
returned [`GenerationResult`](_autosummary/wads.templating.html.md#wads.templating.GenerationResult).

* **Return type:**
  [`GenerationResult`](_autosummary/wads.templating.html.md#wads.templating.GenerationResult)

### wads.templating.make_jinja_env(\*\*overrides)

Build a Jinja2 environment with wads’s custom delimiters.

Extra keyword arguments override the defaults (e.g. `undefined`).

### wads.templating.render(content, context, , env=None)

Render `content` as a Jinja2 template with wads delimiters.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> render("v<< major >>.<< minor >>", {"major": 1, "minor": 2})
'v1.2'
```


# _autosummary/wads.toml_util.html.md

# wads.toml_util

Utilities for reading and writing pyproject.toml files.

### Functions

| [`get_project_metadata`](_autosummary/wads.toml_util.html.md#wads.toml_util.get_project_metadata)(pkg_dir)                | Get the [project] section from pyproject.toml.                           |
|-----------------------------------------------------------------------------------------------|--------------------------------------------------------------------------|
| [`get_project_name`](_autosummary/wads.toml_util.html.md#wads.toml_util.get_project_name)(pkg_dir)                    | Get the project name from pyproject.toml.                                |
| [`get_project_version`](_autosummary/wads.toml_util.html.md#wads.toml_util.get_project_version)(pkg_dir)                 | Get the version from pyproject.toml.                                     |
| [`pep639_license`](_autosummary/wads.toml_util.html.md#wads.toml_util.pep639_license)(license_name)                 | The `[project].license` value for `license_name`, PEP 639 when possible. |
| [`read_pyproject_toml`](_autosummary/wads.toml_util.html.md#wads.toml_util.read_pyproject_toml)(pkg_dir)                 | Read pyproject.toml from the specified package directory.                |
| [`set_project_version`](_autosummary/wads.toml_util.html.md#wads.toml_util.set_project_version)(pkg_dir, version)        | Set the version in pyproject.toml.                                       |
| [`update_project_metadata`](_autosummary/wads.toml_util.html.md#wads.toml_util.update_project_metadata)(pkg_dir, \*\*kwargs) | Update project metadata in pyproject.toml.                               |
| [`update_project_url`](_autosummary/wads.toml_util.html.md#wads.toml_util.update_project_url)(pkg_dir, url[, url_key])  | Update or add a URL in the [project.urls] section of pyproject.toml.     |
| [`write_pyproject_toml`](_autosummary/wads.toml_util.html.md#wads.toml_util.write_pyproject_toml)(pkg_dir, data)          | Write data to pyproject.toml in the specified package directory.         |

### wads.toml_util.get_project_metadata(pkg_dir)

Get the [project] section from pyproject.toml.

* **Parameters:**
  **pkg_dir** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Path to the package directory
* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]
* **Returns:**
  Dictionary containing project metadata

### wads.toml_util.get_project_name(pkg_dir)

Get the project name from pyproject.toml.

* **Parameters:**
  **pkg_dir** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Path to the package directory
* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]
* **Returns:**
  Project name or None if not found

### wads.toml_util.get_project_version(pkg_dir)

Get the version from pyproject.toml.

* **Parameters:**
  **pkg_dir** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Path to the package directory
* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]
* **Returns:**
  Version string or None if not found

### wads.toml_util.pep639_license(license_name)

The `[project].license` value for `license_name`, PEP 639 when possible.

A name that canonicalizes to a valid SPDX expression becomes that string
(`license = "MIT"`). Anything else keeps the deprecated
`{"text": ...}` table, because Hatchling rejects a `license` string
that is not valid SPDX – so this never produces an unbuildable project.
Needs `packaging>=24.2` for SPDX support and falls back to the table
without it.

```pycon
>>> pep639_license("mit")
'MIT'
>>> pep639_license("Apache Software License")
'Apache-2.0'
>>> pep639_license("Proprietary")
{'text': 'Proprietary'}
```

### wads.toml_util.read_pyproject_toml(pkg_dir)

Read pyproject.toml from the specified package directory.

* **Parameters:**
  **pkg_dir** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Path to the package directory
* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]
* **Returns:**
  Dictionary containing the parsed TOML data
* **Raises:**
  * [**ImportError**](https://docs.python.org/3/builtins/exceptions.html#ImportError) – If tomli/tomllib is not available
  * [**FileNotFoundError**](https://docs.python.org/3/builtins/exceptions.html#FileNotFoundError) – If pyproject.toml doesn’t exist

### wads.toml_util.set_project_version(pkg_dir, version)

Set the version in pyproject.toml.

* **Parameters:**
  * **pkg_dir** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Path to the package directory
  * **version** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – New version string
* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### wads.toml_util.update_project_metadata(pkg_dir, \*\*kwargs)

Update project metadata in pyproject.toml.

* **Parameters:**
  * **pkg_dir** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Path to the package directory
  * **\*\*kwargs** – Metadata fields to update
* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### wads.toml_util.update_project_url(pkg_dir, url, url_key='Homepage')

Update or add a URL in the [project.urls] section of pyproject.toml.

* **Parameters:**
  * **pkg_dir** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Path to the package directory
  * **url** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – The URL to set
  * **url_key** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – The key to use in the urls dict (default: “Homepage”)
* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### wads.toml_util.write_pyproject_toml(pkg_dir, data)

Write data to pyproject.toml in the specified package directory.

* **Parameters:**
  * **pkg_dir** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Path to the package directory
  * **data** ([`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]) – Dictionary to write as TOML
* **Raises:**
  [**ImportError**](https://docs.python.org/3/builtins/exceptions.html#ImportError) – If tomli_w is not available
* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)


# _autosummary/wads.user_dirs.html.md

# wads.user_dirs

Platform-appropriate user directories for wads configuration and data.

Provides a facade for user-specific paths that follow platform conventions:

- macOS: ~/Library/Application Support/wads/ (config+data), ~/Library/Caches/wads/ (cache)
- Linux: ~/.config/wads/ (config), ~/.local/share/wads/ (data), ~/.cache/wads/ (cache)
- Windows: %APPDATA%/wads/ (config), %LOCALAPPDATA%/wads/ (data+cache)

User preferences are stored in TOML format at config_dir() / “preferences.toml”.
Name candidate files (plain text, one name per line) go in data_dir() / “name_candidates/”.

### Functions

| [`cache_dir`](_autosummary/wads.user_dirs.html.md#wads.user_dirs.cache_dir)()                   | User cache directory for wads.                                              |
|--------------------------------------------------------------------------------|-----------------------------------------------------------------------------|
| [`config_dir`](_autosummary/wads.user_dirs.html.md#wads.user_dirs.config_dir)()                  | User configuration directory for wads.                                      |
| [`data_dir`](_autosummary/wads.user_dirs.html.md#wads.user_dirs.data_dir)()                    | User data directory for wads.                                               |
| [`name_candidates_dir`](_autosummary/wads.user_dirs.html.md#wads.user_dirs.name_candidates_dir)()         | Directory for name candidate files (plain text, one name per line).         |
| [`read_user_preferences`](_autosummary/wads.user_dirs.html.md#wads.user_dirs.read_user_preferences)()       | Read user preferences from preferences.toml.                                |
| [`user_preferences_path`](_autosummary/wads.user_dirs.html.md#wads.user_dirs.user_preferences_path)()       | Path to the user preferences TOML file.                                     |
| [`write_user_preferences`](_autosummary/wads.user_dirs.html.md#wads.user_dirs.write_user_preferences)(prefs) | Write user preferences to preferences.toml, creating directories as needed. |

### wads.user_dirs.cache_dir()

User cache directory for wads.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### wads.user_dirs.config_dir()

User configuration directory for wads.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### wads.user_dirs.data_dir()

User data directory for wads.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### wads.user_dirs.name_candidates_dir()

Directory for name candidate files (plain text, one name per line).

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### wads.user_dirs.read_user_preferences()

Read user preferences from preferences.toml.

Returns an empty dict if the file doesn’t exist.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

```pycon
>>> prefs = read_user_preferences()
>>> isinstance(prefs, dict)
True
```

### wads.user_dirs.user_preferences_path()

Path to the user preferences TOML file.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### wads.user_dirs.write_user_preferences(prefs)

Write user preferences to preferences.toml, creating directories as needed.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)


# _autosummary/wads.util.html.md

# wads.util

wads util

### Functions

| `clog`(condition[, func])                                                                         |                                                                                      |
|---------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------|
| `ensure_no_slash_suffix`(s)                                                                       |                                                                                      |
| `ensure_slash_suffix`(s)                                                                          |                                                                                      |
| [`find_obj`](_autosummary/wads.util.html.md#wads.util.find_obj)(\*module_and_obj_names)                 | Find an object from specified modules, under specified names.                        |
| [`git`](_autosummary/wads.util.html.md#wads.util.git)([command, work_tree, git_dir])               | Launch git commands.                                                                 |
| [`highlight`](_autosummary/wads.util.html.md#wads.util.highlight)(string[, effect, beg_mark, ...])       | Interprets a string's highlight markers to be able to make highlights in the string. |
| [`import_obj`](_autosummary/wads.util.html.md#wads.util.import_obj)(module_name, obj_name)                | Import an object from a specified module.                                            |
| `is_standard_lib_path`(path)                                                                      |                                                                                      |
| `mk_conditional_logger`(condition[, func])                                                        |                                                                                      |
| [`mk_import_root_replacer`](_autosummary/wads.util.html.md#wads.util.mk_import_root_replacer)(from_to_dict)            | Make a function that does multiple import name replacements.                         |
| [`mk_replacer_from_dict`](_autosummary/wads.util.html.md#wads.util.mk_replacer_from_dict)(from_to_dict)              | Make a function that does multiple replacements (in a single pass).                  |
| [`replace_import_names`](_autosummary/wads.util.html.md#wads.util.replace_import_names)(source_store, from_to_dict) | Replace import names.                                                                |
| `standard_lib_module_names`([...])                                                                |                                                                                      |

### wads.util.find_obj(\*module_and_obj_names)

Find an object from specified modules, under specified names.

This function searches for a specified object in a list of specified modules.
It first tries to import the object from the first module specified, then the
second, and so on, until it finds the object. If the object is not found in any
of the specified modules, an ImportError is raised.

* **Parameters:**
  **\*module_and_obj_names** – A list of pairs of module names and object names.
  The first module specified is searched first, then the second, and so on,
  until the object is found. Each module and object pair is specified as a
  tuple containing two strings. The first string is the module name, and the
  second string is the object name. Alternatively, the module and object pair
  can be specified as a single string containing two space-separated strings,
  where the first string is the module name and the second string is the
  object name.
* **Returns:**
  The imported object.
* **Raises:**
  [**ImportError**](https://docs.python.org/3/builtins/exceptions.html#ImportError) – If the object cannot be found in any of the specified modules.

```pycon
>>> files = find_obj('importlib.resources files', 'importlib_resources files')
>>> callable(files)
True
>>> find_obj(
...     'importlib no_such_obj', 'no_such_pkg x', ['wave.no_such_module', 'y']
... )
Traceback (most recent call last):
...
ImportError: All of these import attempts failed:
    from importlib import no_such_obj
    from no_such_pkg import x
    from wave.no_such_module import y
```

See discussion: [https://github.com/i2mint/wads/discussions/10](https://github.com/i2mint/wads/discussions/10).

### wads.util.git(command='status', work_tree='.', git_dir=None)

Launch git commands.

* **Parameters:**
  * **command** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – git command (e.g. ‘status’, ‘branch’, ‘commit -m “blah”’, ‘push’, etc.)
  * **work_tree** – The work_tree directory (i.e. where the project is)
  * **git_dir** – The .git directory (usually, and by default, will be taken to be “{work_tree}/.git/”
* **Returns:**
  What ever the command line returns (decoded to string)

### wads.util.highlight(string, effect='\\x1b[7m', beg_mark='[[', end_mark=']]', end_effect='\\x1b[0m')

Interprets a string’s highlight markers to be able to make highlights in the string.

This is meant for very simple situations. A more powerful and fast function could be made by
using regular expressions and a map to map “codes” to “effects”.

Try this:

```pycon
>>> print(highlight("This is [[the section]] that is [[highlighted]]."))
```

Above, “reverse” is used as the default effect.
But You can change that to bold blue ink on yellow background. That’s three effects:
1 (for bold), 34, for the blue foreground (ink), and 43 for the “yellow” (more like brown)
background (paper).

```pycon
>>> my_string = "This is [[the section]] that is [[highlighted]]."
>>> print(highlight(my_string, "\033[1;34;43m"))
```

033[whaaaa?!? Yeah… well, either you do it that has-no-life-outside-unicode way.
If so, Ansi help you!
See [this wiki section](https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters).
or [this tutorial](https://www.lihaoyi.com/post/BuildyourownCommandLinewithANSIescapecodes.html#rich-text).

If not, we’ve prepared a map between human language and effect codes in the form of the
`fc` variable of this module. It’s a dict (and if you have `py2store`, it’s a mapping containing
that dict and allowing you access through attributes too).

```pycon
>>> from wads.util import fc
>>> list(fc)[20:25]
['magenta', 'cyan', 'gray', 'dark_gray', 'dark_red']
```

* **Parameters:**
  * **string** – String with highlight formatting
  * **effect** – The effect to use for the highlighting (some unicode like “033[21m”)
  * **beg_mark** – String that marks the beginning of the highlight
  * **end_mark** – String that marks the end of the highlight
  * **end_effect** – The unicode to use to reset the effect
* **Returns:**

### wads.util.import_obj(module_name, obj_name)

Import an object from a specified module.

* **Parameters:**
  * **from** ([*str*](https://docs.python.org/3/builtins/stdtypes.html#str)) – The name of the module to import from.
  * **obj_name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – The name of the object to import.
* **Returns:**
  The imported object.
* **Raises:**
  * [**ImportError**](https://docs.python.org/3/builtins/exceptions.html#ImportError) – If the module or object cannot be imported.
  * [**AttributeError**](https://docs.python.org/3/builtins/exceptions.html#AttributeError) – If the object does not exist in the module.

```pycon
>>> spec_finder = import_obj('importlib.util', 'find_spec')
>>> callable(spec_finder)
True
>>> spec_finder.__module__
'importlib.util'
```

### wads.util.mk_import_root_replacer(from_to_dict)

Make a function that does multiple import name replacements.

For a use case, see replace_import_names. This is just a helper function.

```pycon
>>> replace = mk_import_root_replacer({'foo': 'FOO', 'bar': 'BAR'})
>>>
>>> assert replace('from foo import BLAH') == 'from FOO import BLAH'
>>> assert replace('from foo.bar import BLAH') == 'from FOO.bar import BLAH'
>>> assert replace('import bar') == 'import BAR'
```

Partial matches are not replaced (that’s a good thing!):

```pycon
>>> assert replace('import barmitzvah as oy') == 'import barmitzvah as oy'
>>> assert replace('from foobar import hello') == 'from foobar import hello'
```

Yes, and it works with dotpaths:

```pycon
>>> replace = mk_import_root_replacer({'where.it.was': 'where.it.is.now'})
>>> replace('import where.it.was as here')
'import where.it.is.now as here'
```

* **Parameters:**
  **from_to_dict** – A dict of {to_find: to_replace_by,…} pairs
* **Returns:**
  A replacer function that you can apply to strings to carry out the replacements

### wads.util.mk_replacer_from_dict(from_to_dict)

Make a function that does multiple replacements (in a single pass).

```pycon
>>> r = mk_replacer_from_dict({'is': 'are', 'life': 'butterflies'})
>>> r("There is no life in the void.")
'There are no butterflies in the void.'
```

* **Parameters:**
  **from_to_dict** – A dict of {to_find: to_replace_by,…} pairs
* **Returns:**
  A replacer function that you can apply to strings to carry out the replacements

### wads.util.replace_import_names(source_store, from_to_dict, target_store=None, dryrun=True, verbose=True, replacer_factory=<function mk_import_root_replacer>, add_comment_at_the_end_of_lines_replaced=False)

Replace import names.

Use case: You’ve renamed something or moved some modules (remember UNIX? Same as move!) and have to go through
all your files and notebooks and replace those names.
Now, if you have a nice IDE, we suggest you use refactoring instead – as long as you have any uses in the scope.
But sometimes it’s not enough. You might have text/html documments, or jupyter notebooks, etc.
So you can use this instead.

Be warned though:

- You should look at the pattern that is used to match, and make sure it won’t create havoc.
- Backup your documents so you can revert!
- Print matches before you actually apply them all in bulk.
- Use at your own risk!

For examples, see the mk_import_root_replacer helper function.

* **Parameters:**
  * **source_store**
  * **from_to_dict** – A dict of {to_find: to_replace_by,…} pairs
  * **target_store** – The store
  * **dryrun**
  * **verbose**
  * **replacer_factory** – makes the replacer = replacer_factory(from_to_dict)
  * **add_comment_at_the_end_of_lines_replaced** – True/False or an actual string to add at the end of replaced lines
* **Returns:**


# about-this-build.html.md

<!-- generated by epythet -->

# About this build

This documentation was built on **2026-10-13 07:12 UTC** from commit <a href="https://github.com/i2mint/wads/commit/25586fc819d038532c2d435decbfe1181e3579a3"><code>25586fc</code></a> on branch <code>master</code>, for **wads 0.2.38** (from <code>pyproject.toml</code>).

#### WARNING
The documentation and the package may be misaligned:

- The documented version (0.2.38) is behind the latest release on PyPI (0.2.39): `pip install wads` gives newer code than these docs describe.

## Source

|                     |                                                                                                                                                    |
|---------------------|----------------------------------------------------------------------------------------------------------------------------------------------------|
| Commit              | <a href="https://github.com/i2mint/wads/commit/25586fc819d038532c2d435decbfe1181e3579a3"><code>25586fc819d038532c2d435decbfe1181e3579a3</code></a> |
| Branch              | <code>master</code>                                                                                                                                |
| Tags at this commit | none                                                                                                                                               |
| Working tree        | clean                                                                                                                                              |
| Remote              | <code>https://github.com/i2mint/wads</code>                                                                                                        |

## Continuous integration

|              |                                                                                            |
|--------------|--------------------------------------------------------------------------------------------|
| Repository   | <code>i2mint/wads</code>                                                                   |
| Run          | <a href="https://github.com/i2mint/wads/actions/runs/38360977812">38360977812</a>          |
| Ref          | <code>refs/heads/master</code>                                                             |
| Event commit | <code>25586fc819d038532c2d435decbfe1181e3579a3</code> (in the history of the built commit) |

## Tools

|          |         |
|----------|---------|
| epythet  | 0.2.14  |
| Sphinx   | 9.1.0   |
| docutils | 0.22.4  |
| Python   | 3.12.15 |

## Configuration as resolved

|               |                                                                  |
|---------------|------------------------------------------------------------------|
| theme         | <code>auto</code> (Sphinx theme <code>furo</code>)               |
| accent        | <code>#00648f</code>                                             |
| api_generator | <code>autosummary</code>                                         |
| ignore        | <code>tests/</code>, <code>scrap/</code>, <code>examples/</code> |
| agent_outputs | <code>true</code>                                                |
| aggregates    | <code>md</code>                                                  |
| ai_artifacts  | <code>true</code>                                                |

## Package on PyPI

Latest release: <a href="https://pypi.org/project/wads/0.2.39/">0.2.39</a>, newer than the documented version (0.2.38).

## Reproduce

```bash
git clone https://github.com/i2mint/wads && cd wads
git checkout 25586fc819d038532c2d435decbfe1181e3579a3
pip install "epythet==0.2.14"
epythet quickstart . --ignore tests/ scrap/ examples/
```

The same data, for machines: <a href="build_info.json"><code>build_info.json</code></a> (schema version 1).


# ai-agents.html.md

<!-- generated by epythet -->

# For AI agents

`wads` ships artifacts for coding agents alongside its code. This page lists
them, says where each lives in the repository, and points at the
machine-readable copies of this documentation.

## Skills

Skills are folders holding a `SKILL.md` (the [Agent Skills](https://agentskills.io) format): a description that tells an agent when to use it and a body with the procedure. Install one into your agent with `gh skill` (any host: `--agent claude-code`, `copilot`, `cursor`, `codex`, `gemini`), or use the copy bundled in the wheel.

### `setup-py-project`

Use when the user wants to create a new Python project/package, name a project, set up a GitHub repo, or bootstrap a project from scratch. Triggers on requests like “help me set up a project”, “create a package”, “I need a name for…”, “new repo for…”.

```bash
gh skill install i2mint/wads setup-py-project --agent claude-code
```

Source: [`wads/data/skills/setup-py-project`](https://github.com/i2mint/wads/tree/HEAD/wads/data/skills/setup-py-project) (bundled with the pip package).

### `wads-changelog`

Generate and maintain CHANGELOG.md for a wads-managed repo (one where CI auto-bumps the version and tags releases with bare version tags like 0.2.3) from git history. Diagnoses coverage (does a changelog exist, latest tag vs latest version covered, gap list), generates Keep-a-Changelog-style sections per version tag with CI noise filtered (bump commits, [skip ci] housekeeping, bare merges) and PR titles kept verbatim, and maintains the file idempotently: only inserts sections for missing versions, never rewrites hand-edits. Use for “generate a changelog”, “create CHANGELOG.md”, “update the changelog”, “is the changelog up to date”, “backfill the changelog”, “what changed in 0.2.3”, “release notes from git history”, “changelog is behind the releases”. Also trigger when wads-repo-doctor reports a missing/stale changelog. Does NOT add the [project.urls].Changelog link or polish PyPI metadata (wads-pypi-polish), and does NOT diagnose version-sync or CI problems (wads-ci-health).

```bash
gh skill install i2mint/wads wads-changelog --agent claude-code
```

Source: [`wads/data/skills/wads-changelog`](https://github.com/i2mint/wads/tree/HEAD/wads/data/skills/wads-changelog) (bundled with the pip package).

### `wads-ci-health`

Point-in-time CI health snapshot for a single wads-managed Python repo (one using the i2mint uv-ci stub or inline uv workflow). Checks five things and reports a checklist: is CI green, is ci.yml the current stub (and how it’s pinned), are the secrets wired correctly across the two layers, is the version in sync between pyproject/PyPI/git tags, and is docs publishing (Pages, Discussions) healthy. Use when the user asks “is CI green”, “check CI health”, “why didn’t it publish”, “is this repo’s CI up to date”, “are my CI secrets set up right”, “is PyPI in sync with pyproject”, “give me a CI snapshot”, or “health-check this repo’s CI”. Diagnosis only — fixes are delegated: format migration → wads-migrate, push-back/version-sync repair → wads-ci-fix, fleet/batch work → wads-ci-sweep, Pages repair → epythet-docs. Does NOT cover generic CI strategy or non-wads pipelines — use ci-advisor or ci-setup for those.

```bash
gh skill install i2mint/wads wads-ci-health --agent claude-code
```

Source: [`wads/data/skills/wads-ci-health`](https://github.com/i2mint/wads/tree/HEAD/wads/data/skills/wads-ci-health) (bundled with the pip package).

### `wads-docs-coverage`

Documentation coverage for a wads-managed Python repo: diagnose, align, complete. Measures docstring presence (every module, class, and function), executes the README’s code blocks to prove they still run, cross-checks README names and install lines against the live API, and detects signature↔docstring drift; then proposes removals for obsolete docs and writes missing Google-style docstrings (with doctests) and README sections. Use when the user says “audit the docs”, “docs coverage”, “what’s missing docstrings”, “write the missing docstrings”, “is the README up to date”, “check the README examples”, “the README mentions things that don’t exist”, or “docstring coverage report”. Also trigger after a refactor or rename that may have stranded docs, or when CI lint flags D1xx missing-docstring errors. Does NOT fix RST/Sphinx rendering mistakes (use wads-docstring-render), does NOT cover docs publishing, GitHub Pages, or 404s (use epythet-docs), and does NOT measure test coverage (use wads-test-coverage).

```bash
gh skill install i2mint/wads wads-docs-coverage --agent claude-code
```

Source: [`wads/data/skills/wads-docs-coverage`](https://github.com/i2mint/wads/tree/HEAD/wads/data/skills/wads-docs-coverage) (bundled with the pip package).

### `wads-docstring-render`

Find and fix docstrings that mis-render under epythet (Sphinx + RST + napoleon): single backticks showing as italics, doctests glued into paragraphs, literal “Returns:” or “:param” text, red asterisks from bare \*args, collapsed bullet lists, markdown fences, smartquote-corrupted code. Use for “fix my docstrings”, “the API docs render wrong”, “my doctest shows as plain text”, “why is this italic in the docs”, “red asterisks in the docs”, “Returns: shows literally on the page”, “audit docstring RST”, “make docstrings Sphinx-clean”, “the published module page looks mangled”. Also trigger after writing new docstrings or before a docs release. Does NOT cover docs publishing, GitHub Pages, or 404s (use epythet-docs), and does NOT measure docstring presence/coverage or README drift (use wads-docs-coverage).

```bash
gh skill install i2mint/wads wads-docstring-render --agent claude-code
```

Source: [`wads/data/skills/wads-docstring-render`](https://github.com/i2mint/wads/tree/HEAD/wads/data/skills/wads-docstring-render) (bundled with the pip package).

### `wads-import-time`

Audit and improve import time and import hygiene in a wads-managed Python repo. Measures `python -X importtime` (warm-cache, against the local checkout), ranks heavy modules and third-party offenders, verifies every module imports standalone (CI runs pytest –doctest-modules, which imports every module), cross-checks unguarded module-level imports of undeclared/extra-only packages, then makes imports lazy and clean (function-local imports, TYPE_CHECKING, PEP 562 module **getattr**, guarded optional deps). Use for “why is import so slow”, “speed up import”, “make imports lazy”, “audit imports”, “import hygiene check”, “this module breaks –doctest-modules”, “optional dependency breaks import”, “lighten the core install”. Also trigger when CI fails at collection with ImportError/ModuleNotFoundError before any test runs. Does NOT do full declared-vs-imported dependency drift (use the wads-deps CLI), measure test coverage (wads-test-coverage), or diagnose CI wiring (wads-ci-health).

```bash
gh skill install i2mint/wads wads-import-time --agent claude-code
```

Source: [`wads/data/skills/wads-import-time`](https://github.com/i2mint/wads/tree/HEAD/wads/data/skills/wads-import-time) (bundled with the pip package).

### `wads-migrate`

Use when migrating Python projects to modern wads setup (pyproject.toml + uv CI). Triggers on migration tasks, CI modernization, setup.cfg conversion, switching from pip to uv, or moving a repo from inline uv CI to the reusable-workflow stub.

```bash
gh skill install i2mint/wads wads-migrate --agent claude-code
```

Source: [`wads/data/skills/wads-migrate`](https://github.com/i2mint/wads/tree/HEAD/wads/data/skills/wads-migrate) (bundled with the pip package).

### `wads-pypi-polish`

Audit and improve a package’s pyproject.toml so its PyPI page looks professional, is discoverable in search, and signals quality. Use when the user wants to “polish pyproject.toml”, “make my PyPI page look professional”, “improve PyPI discoverability”, “add classifiers / keywords / project URLs”, “add badges”, “review my package metadata”, “why does my PyPI page look bare”, or prepare a package for publishing/release. Adds and validates description, readme, license (SPDX), requires-python, keywords, trove classifiers, and project.urls with PyPI-recognized labels; recommends README badges and quality signals (coverage, lint, typing). Does NOT change code, dependencies, or versions; CI/publishing state belongs to wads-ci-health, docs publishing (Pages/Sphinx) to epythet-docs.

```bash
gh skill install i2mint/wads wads-pypi-polish --agent claude-code
```

Source: [`wads/data/skills/wads-pypi-polish`](https://github.com/i2mint/wads/tree/HEAD/wads/data/skills/wads-pypi-polish) (bundled with the pip package).

### `wads-repo-doctor`

Full health diagnosis and improvement dispatch for a wads-managed Python repo (uv-ci stub, i2mint conventions). One read-only audit across every dimension — legacy packaging, CI generation and status, PyPI/pyproject version sync, PyPI metadata, tests, docstrings, docs publishing, GitHub metadata drift, type annotations/py.typed, import time, changelog, agent skills — then a prioritized health report and dispatch to specialist skills one dimension at a time. Use for “diagnose this repo”, “repo health check”, “what’s wrong with this repo”, “audit this package”, “doctor this repo”, “how healthy is this project”, “bring this repo up to standard”, “what should I improve here”. Also trigger when the user names a repo and wants a general improvement pass. Does NOT itself migrate, debug CI, write tests, or fix docstrings — it diagnoses, prioritizes, and routes to the owning specialist skills. Only GitHub metadata alignment is fixed inline.

```bash
gh skill install i2mint/wads wads-repo-doctor --agent claude-code
```

Source: [`wads/data/skills/wads-repo-doctor`](https://github.com/i2mint/wads/tree/HEAD/wads/data/skills/wads-repo-doctor) (bundled with the pip package).

### `wads-skillify`

The skills step of a wads repo-improvement pass: assess whether a repo warrants AI agent skills (baseline: a consumer + a dev skill) and dispatch the work. Layout, distribution, and spec compliance are DEFERRED to the canonical authority — the `skill-package-setup` skill in the `skill` package. Use when the user says “skillify this repo”, “does this repo need skills”, “set up skills for this project”, “where should skills live”, “restructure the skills layout”, “align/validate my skills”, “make skills gh-skill compatible”, or “why doesn’t Claude Code see this repo’s skills”. Also trigger as the skills step of a repo health pass (wads-repo-doctor). Does NOT author skill content (skill-build / skill-creator), write README docs (skill-docs), or fix content drift (skill-sync).

```bash
gh skill install i2mint/wads wads-skillify --agent claude-code
```

Source: [`wads/data/skills/wads-skillify`](https://github.com/i2mint/wads/tree/HEAD/wads/data/skills/wads-skillify) (bundled with the pip package).

### `wads-test-coverage`

Diagnose and close test-coverage gaps in a wads-managed Python repo (pytest + doctests + uv CI). Measures coverage exactly as CI does (doctests included), adds branch coverage and per-test contexts, buckets every public function/class as untested / doctest-only / test-covered, then writes the missing tests. Use for “what’s untested”, “check test coverage”, “coverage gaps”, “which functions have no tests”, “is this only covered by doctests”, “improve coverage”, “raise coverage”, “write tests for the uncovered parts”, “add tests for this module”. Also trigger when CI coverage numbers look wrong or a doctest-heavy package looks untested. Does NOT analyze failing-test output (pipe pytest output to the wads-test-analyze CLI), measure docstring presence (wads-docs-coverage), or check CI workflow health (wads-ci-health).

```bash
gh skill install i2mint/wads wads-test-coverage --agent claude-code
```

Source: [`wads/data/skills/wads-test-coverage`](https://github.com/i2mint/wads/tree/HEAD/wads/data/skills/wads-test-coverage) (bundled with the pip package).

### `wads-type-coverage`

Type-annotation coverage for a wads-managed Python repo: diagnose, complete, decide py.typed. Measures public-API type completeness with pyright –verifytypes (per-symbol score), produces an agent-actionable missing-annotation work list (ruff ANN rules, JSON), checks whether py.typed exists and actually ships in the wheel, then writes the missing annotations duck-typing-first (collections.abc, Protocol) and advises when adding py.typed is justified. Use for “type coverage”, “how typed is this package”, “add type annotations”, “annotate the public API”, “should I add py.typed”, “is this package typed”, “type completeness score”, “make this PEP 561 compliant”, “mypy strict cleanup”. Also trigger when downstream users report missing-stub or Unknown-type complaints about the package. Does NOT set the Typing :: Typed classifier or other PyPI metadata (wads-pypi-polish), measure test or docstring coverage (wads-test-coverage / wads-docs-coverage), or fix import-time slowness found along the way (wads-import-time).

```bash
gh skill install i2mint/wads wads-type-coverage --agent claude-code
```

Source: [`wads/data/skills/wads-type-coverage`](https://github.com/i2mint/wads/tree/HEAD/wads/data/skills/wads-type-coverage) (bundled with the pip package).

### `wads-dev-workflow`

How to change the wads package itself (i2mint/wads) safely: install the right extras, run the test suite exactly the way CI does (package doctests included, on Python 3.10/3.11/3.12), regenerate the populate goldens after an intended output change, exercise the git-commit push-back script, and keep the reusable workflow, actions, stub template and secrets superset in sync. Use when editing wads source, actions/\*, .github/workflows/uv-ci.yml, wads/data templates or the shipped skills, when a wads test fails, when “regenerate the goldens”, “run wads tests like CI”, or “is this wads change safe to merge” comes up. Every merge to master publishes wads to PyPI and goes live for every repo whose stub floats on @master. Not for USING wads in another repo (see wads-migrate, wads-ci-health, setup-py-project).

```bash
gh skill install i2mint/wads wads-dev-workflow --agent claude-code
```

Source: [`skills/wads-dev-workflow`](https://github.com/i2mint/wads/tree/HEAD/skills/wads-dev-workflow).

The bundled skills are also on disk after `pip install wads`, under the package’s `data/skills/` directory; link them into an agent without network access with `skill link-skills <that directory>`.

## Instruction files

Files agents read before working in this repository.

- [`.claude/CLAUDE.md`](https://github.com/i2mint/wads/tree/HEAD/.claude/CLAUDE.md): read by Claude Code

## Machine-readable documentation

This site publishes the same documentation in forms that fit an agent’s context window:

- [`llms.txt`](https://i2mint.github.io/wads/llms.txt): an index of every page with a one-line description ([llms.txt](https://llmstxt.org) format)
- [`wads.md`](https://i2mint.github.io/wads/wads.md): the whole documentation as one Markdown file
- `<page>.html.md`: a rendered Markdown twin of every page, advertised from each page’s `<head>` with `<link rel="alternate" type="text/markdown">`
- [`objects.inv`](https://i2mint.github.io/wads/objects.inv): the Sphinx inventory: a symbol-to-URL index (`sphobjinv convert plain objects.inv -`)


# api.html.md

# API reference

| [`wads`](_autosummary/wads.html.md#module-wads)   | Console Scripts   |
|---------------------------------------------------------------------|-------------------|


