Metadata-Version: 2.4
Name: ft-readiness
Version: 0.1.0
Summary: Author-side pre-publish gate for free-threaded Python (3.14t): catch the silently re-enabled GIL, a missing cp314t wheel, and a stale Free Threading trove classifier before you ship.
Project-URL: Homepage, https://github.com/fernforge/ft-readiness
Project-URL: Source, https://github.com/fernforge/ft-readiness
Project-URL: Issues, https://github.com/fernforge/ft-readiness/issues
Author: fernforge
License-Expression: MIT
License-File: LICENSE
Keywords: ci,cp314t,free-threaded,free-threading,gil,linter,packaging,pytest,python-3.14,wheels
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Framework :: Pytest
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: Free Threading :: 3 - Stable
Classifier: Topic :: Software Development :: Build Tools
Classifier: Topic :: Software Development :: Quality Assurance
Requires-Python: >=3.9
Requires-Dist: packaging>=21.0
Provides-Extra: tomli
Requires-Dist: tomli>=1.1.0; (python_version < '3.11') and extra == 'tomli'
Description-Content-Type: text/markdown

# ft-readiness

Catch a broken free-threading build before you publish it, not after your users file the bug.

Python 3.14 made free-threading officially supported. If you ship a C extension there are three ways to break it silently — and none of them fail your build:

1. Your extension omits the `Py_mod_gil` slot. CPython **re-enables the GIL** the moment someone imports your module on a free-threaded interpreter. Every 3.14t user loses parallelism; the only signal is a warning buried in stderr.
2. You ship wheels for regular CPython but **no `cp314t` wheel**. Free-threaded users fall back to an sdist build, or to the GIL build, with nothing telling you they got the slow path.
3. Your `Free Threading ::` trove classifier says one thing and your wheels say another — you advertise support you didn't ship, or ship support nobody can discover on PyPI.

`ft-readiness` is a pre-publish gate for all three. Run it in CI after `python -m build`; it exits non-zero when your artifacts and your claims don't line up.

```console
$ ft-readiness .
✗ FT001 [error] Declares 'Programming Language :: Python :: Free Threading :: 2 - Beta' but ships no free-threaded (cp3XXt) wheel for its compiled extension.
    → Build a cp3XXt wheel (cibuildwheel with CIBW_ENABLE=cpython-freethreading, or CPython 3.14t + your build backend), or drop the Free Threading classifier until you ship one.
! FT004 [warning] Ships abi3 (limited-API) wheels but no cp3XXt wheel. The stable ABI is unsupported on the free-threaded build, so abi3 wheels do NOT cover 3.14t users.
    → Build a separate free-threaded wheel; you can keep abi3 for the GIL build (e.g. py_limited_api=not sysconfig.get_config_var('Py_GIL_DISABLED')).

ft-readiness 0.1.0: 1 error(s), 1 warning(s).
```

## Install

```console
pip install ft-readiness
```

Python 3.9+. Pure Python, one runtime dependency (`packaging`).

## What it checks

The static checks read your built `dist/` and your `pyproject.toml` (or a wheel's `METADATA`). They run on any interpreter — you do **not** need a free-threaded build to run them.

| Code | Level | Fires when |
|------|-------|-----------|
| FT001 | error | A `Free Threading ::` classifier is declared, the package has a compiled extension, but no `cp3XXt` wheel ships. You advertise support your artifacts don't deliver. |
| FT002 | warning | A `cp3XXt` wheel ships but no `Free Threading ::` classifier is declared. Support is real but undiscoverable on PyPI. |
| FT003 | warning | The package ships compiled-extension wheels but none for free-threaded Python. The silent missing-wheel case. |
| FT004 | warning | Only `abi3` (limited-API) wheels ship, no `cp3XXt`. The stable ABI is unsupported on the free-threaded build, so abi3 does not cover 3.14t. |
| FT005 | warning | The package advertises free-threading but `requires-python` excludes the versions (3.13+) that have a free-threaded build. |
| FT006 | info | A pure-Python package claims `3 - Stable`/`4 - Resilient` — a reminder to back the claim with thread-safety tests. |

The **runtime** check is the one that catches a re-enabled GIL, and it needs a free-threaded interpreter (it imports your module and watches the GIL state):

| Code | Level | Fires when |
|------|-------|-----------|
| FT010 | error | Importing a module you named re-enabled the GIL — the `Py_mod_gil` slot is missing. |
| FT011 | warning | A module named for the runtime check couldn't be imported. |

```console
# on a python3.14t interpreter, in CI:
ft-readiness . --import mypkg._core --import mypkg._fast
```

On a non-free-threaded interpreter the `--import` checks are skipped with a note, so the same command is safe everywhere in your matrix.

## pytest plugin

If you already run a 3.14t leg in CI, wire the runtime check into pytest instead:

```console
pytest --ft-import mypkg._core --ft-import mypkg._fast
```

Each module is imported in a fresh subprocess and reported as its own test. On a non-free-threaded build the checks skip.

## GitHub Action

```yaml
- uses: fernforge/ft-readiness@v1
  with:
    project: .        # optional, default '.'
    strict: 'false'   # optional, treat warnings as errors
```

The action installs `ft-readiness` and runs the static checks against your `dist/`. Run it after your build step. For the runtime GIL check, add a job on `actions/setup-python` with a `3.14t` interpreter and call `ft-readiness --import ...` there.

## Why these three failures and not others

The GIL re-enable prints a warning, so an attentive porter can catch it by reading stderr. The two failures with *no* signal at all are the missing `cp314t` wheel and the classifier that doesn't match the wheels — a user just gets the slow path and never tells you. ft-readiness turns all three into a build failure you see before the release goes out.

## Options

```
ft-readiness [PROJECT] [--dist DIR] [--import MODULE ...] [--strict] [--format text|json]
```

- `PROJECT` — project directory (default `.`); its `dist/` is inspected unless `--dist` is given.
- `--dist DIR` — directory of built wheels/sdists.
- `--import MODULE` — runtime GIL check for a module (repeatable or comma-separated).
- `--strict` — exit 1 on warnings too.
- `--format json` — machine-readable output for CI dashboards.

Exit code is 1 if there are any errors (or any warnings under `--strict`), else 0.

## License

MIT
