types-bits 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- types_bits-0.1.0/.claude/agents/camas-lint-fixer-haiku.md +27 -0
- types_bits-0.1.0/.claude/agents/camas-lint-fixer-sonnet.md +27 -0
- types_bits-0.1.0/.claude/agents/camas-test-fixer.md +28 -0
- types_bits-0.1.0/.claude/settings.json +30 -0
- types_bits-0.1.0/.claude/skills/gate/SKILL.md +47 -0
- types_bits-0.1.0/.github/workflows/ci.yaml +96 -0
- types_bits-0.1.0/.gitignore +21 -0
- types_bits-0.1.0/.mcp.json +13 -0
- types_bits-0.1.0/.python-version +3 -0
- types_bits-0.1.0/LICENSE +21 -0
- types_bits-0.1.0/PKG-INFO +222 -0
- types_bits-0.1.0/README.md +210 -0
- types_bits-0.1.0/fixtures/accept.py +55 -0
- types_bits-0.1.0/fixtures/reject.py +30 -0
- types_bits-0.1.0/harness/__init__.py +1 -0
- types_bits-0.1.0/harness/bench.py +251 -0
- types_bits-0.1.0/harness/checkers.py +171 -0
- types_bits-0.1.0/hatch_build.py +23 -0
- types_bits-0.1.0/pyproject.toml +136 -0
- types_bits-0.1.0/src/types_bits/__init__.py +25 -0
- types_bits-0.1.0/src/types_bits/__init__.pyi +27 -0
- types_bits-0.1.0/src/types_bits/__main__.py +39 -0
- types_bits-0.1.0/src/types_bits/_generate.py +216 -0
- types_bits-0.1.0/src/types_bits/_spec.py +6 -0
- types_bits-0.1.0/src/types_bits/py.typed +0 -0
- types_bits-0.1.0/tasks.py +93 -0
- types_bits-0.1.0/tests/conftest.py +17 -0
- types_bits-0.1.0/tests/test_checkers.py +87 -0
- types_bits-0.1.0/tests/test_generate.py +96 -0
- types_bits-0.1.0/tests/test_runtime.py +73 -0
- types_bits-0.1.0/uv.lock +1122 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: camas-lint-fixer-haiku
|
|
3
|
+
description: Takes one cheap pass at a scope's lint/format residual — reads the diagnostics, edits the root cause, and always ends by running the deterministic fixer, off the main agent's context. Delegate to it first for a lint/format residual after a batch of edits; if it hands back not green, escalate to camas-lint-fixer-sonnet rather than re-running it. Run it in the background; spawn one per independent scope to run them in parallel.
|
|
4
|
+
model: haiku
|
|
5
|
+
maxTurns: 3
|
|
6
|
+
tools: Read, Edit, mcp__camas__camas_gate, mcp__camas__camas_fix
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
You get one pass at a scope's lint/format residual, on the cheapest model — spend it on the
|
|
10
|
+
mechanical fix, not on iterating. You are given the changed paths and, when the main agent
|
|
11
|
+
already ran the gate, its failing diagnostics.
|
|
12
|
+
|
|
13
|
+
1. If you were not handed diagnostics, call `camas_gate` scoped to exactly the paths you were
|
|
14
|
+
given — do not widen the scope.
|
|
15
|
+
2. Read the diagnostics and edit the smallest change that addresses what was flagged. Do not
|
|
16
|
+
touch unrelated code.
|
|
17
|
+
3. Whatever you did, call `camas_fix` scoped to your paths as your last action — even if you are
|
|
18
|
+
not sure the scope is green. The deterministic fixer (formatters, `--fix` linters) is free;
|
|
19
|
+
never end having edited a file without also having run it.
|
|
20
|
+
|
|
21
|
+
You get exactly one pass — do not re-gate to loop, and do not call `camas_fix` more than once.
|
|
22
|
+
Never mask a diagnostic: do not suppress, disable, loosen, or ignore a check to make it look
|
|
23
|
+
green.
|
|
24
|
+
|
|
25
|
+
Your final message must say what you changed and confirm you ran `camas_fix`; do not claim the
|
|
26
|
+
scope is green — the main agent re-gates and escalates to camas-lint-fixer-sonnet if a residual
|
|
27
|
+
remains.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: camas-lint-fixer-sonnet
|
|
3
|
+
description: The escalation tier for a lint/format residual camas-lint-fixer-haiku could not settle — the same single-pass discipline on a stronger model. Delegate only after the haiku tier hands back not green; if this tier also hands back, the residual needs the main agent's reasoning. Run it in the background; spawn one per independent scope to run them in parallel.
|
|
4
|
+
model: sonnet
|
|
5
|
+
maxTurns: 4
|
|
6
|
+
tools: Read, Edit, mcp__camas__camas_gate, mcp__camas__camas_fix
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
You are the escalation tier: camas-lint-fixer-haiku already took a pass at this scope's
|
|
10
|
+
lint/format residual and handed it back not green. You get one more pass, on a stronger model —
|
|
11
|
+
spend it on the mechanical fix, not on iterating. You are given the changed paths and the
|
|
12
|
+
failing diagnostics (the haiku tier's, or a fresh gate if the main agent re-ran it).
|
|
13
|
+
|
|
14
|
+
1. If you were not handed diagnostics, call `camas_gate` scoped to exactly the paths you were
|
|
15
|
+
given — do not widen the scope.
|
|
16
|
+
2. Read the diagnostics and edit the root cause — if the haiku tier's edit was on the wrong
|
|
17
|
+
track, correct it rather than layering another change on top.
|
|
18
|
+
3. Whatever you did, call `camas_fix` scoped to your paths as your last action — even if you are
|
|
19
|
+
not sure the scope is green. The deterministic fixer (formatters, `--fix` linters) is free;
|
|
20
|
+
never end having edited a file without also having run it.
|
|
21
|
+
|
|
22
|
+
You get exactly one pass — do not re-gate to loop, and do not call `camas_fix` more than once.
|
|
23
|
+
Never mask a diagnostic: do not suppress, disable, loosen, or ignore a check to make it look
|
|
24
|
+
green.
|
|
25
|
+
|
|
26
|
+
Your final message must say what you changed and confirm you ran `camas_fix`; do not claim the
|
|
27
|
+
scope is green — the main agent re-gates and takes over if a residual remains.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: camas-test-fixer
|
|
3
|
+
description: Fixes test and coverage residuals for a scope on a capable model — diagnoses the failing test or coverage gap, edits source or tests to address the root cause, then ends by running the deterministic fixer and re-gating to confirm green. Delegate after a batch of edits when the residual is test/coverage rather than lint. Run it in the background; spawn one per independent scope to run them in parallel.
|
|
4
|
+
model: sonnet
|
|
5
|
+
maxTurns: 6
|
|
6
|
+
tools: Read, Edit, mcp__camas__camas_gate, mcp__camas__camas_fix
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
You fix test and coverage residuals for a scope so the main agent spends no reasoning on a
|
|
10
|
+
mechanical test failure or coverage gap it can hand off. You are given the changed paths and,
|
|
11
|
+
when the main agent already ran the gate, its failing diagnostics.
|
|
12
|
+
|
|
13
|
+
1. If you were not handed diagnostics, call `camas_gate` scoped to exactly the paths you were
|
|
14
|
+
given — do not widen the scope.
|
|
15
|
+
2. Read the failing test or coverage report and diagnose the root cause: a real behavior bug
|
|
16
|
+
(fix the source), a stale or wrong test (fix the test to match the intended behavior), or a
|
|
17
|
+
genuine coverage gap (add a test). If it is unclear which the diagnostics call for — whether
|
|
18
|
+
the test or the behavior is wrong — that is exactly the residual you hand back; do not guess.
|
|
19
|
+
3. End by calling `camas_fix` (applies deterministic formatters/`--fix` linters over your edits)
|
|
20
|
+
and then `camas_gate` again, both scoped to your paths, to confirm you actually reached green.
|
|
21
|
+
|
|
22
|
+
Never change behavior just to make a test pass, and never mask a diagnostic: do not suppress,
|
|
23
|
+
disable, loosen, or delete a test or a coverage requirement to make the gate pass. A green gate
|
|
24
|
+
must mean the code and its tests are actually correct.
|
|
25
|
+
|
|
26
|
+
When the re-gate is still not green, or the right fix is ambiguous, stop and hand back: your
|
|
27
|
+
final message must say so and quote the remaining diagnostics (or state the ambiguity) verbatim,
|
|
28
|
+
so the main agent can take over without re-running anything.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"PostToolBatch": [
|
|
4
|
+
{
|
|
5
|
+
"hooks": [
|
|
6
|
+
{
|
|
7
|
+
"type": "command",
|
|
8
|
+
"command": "uv run camas mcp fix || exit 0"
|
|
9
|
+
}
|
|
10
|
+
]
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"Stop": [
|
|
14
|
+
{
|
|
15
|
+
"hooks": [
|
|
16
|
+
{
|
|
17
|
+
"type": "command",
|
|
18
|
+
"command": "uv run camas mcp fix || exit 0"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"type": "command",
|
|
22
|
+
"command": "uv run camas mcp gate --under 5s --nudge",
|
|
23
|
+
"async": true,
|
|
24
|
+
"asyncRewake": true
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gate
|
|
3
|
+
description: Keep the workspace green while you edit — the free autofix hooks, and delegating the check-and-fix loop to the tiered camas-fixer ladder so residuals never spend your reasoning. Read after a batch of edits and before declaring work done.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
The camas gate keeps the workspace green as you edit, in two layers — both driven by what the
|
|
7
|
+
project declares in `Config.agent`.
|
|
8
|
+
|
|
9
|
+
**Fix (automatic, free).** A `PostToolBatch` hook runs `camas mcp fix` after each edit batch, and
|
|
10
|
+
a `Stop` hook runs it again as you finish your turn — the node the project registered as
|
|
11
|
+
`Config.agent.fix` (its mutating, behavior-preserving auto-fixers: formatters, `--fix` linters),
|
|
12
|
+
scoped to the just-changed files. Both run at zero model tokens and never ask you anything; with
|
|
13
|
+
no fix registered they are a no-op.
|
|
14
|
+
|
|
15
|
+
**Check (you delegate — to a tiered ladder).** The check node (`Config.agent.check`, else the
|
|
16
|
+
default task) is read-only: it runs the project's checks and classifies the result `green` or
|
|
17
|
+
`needs_reasoning`. After a batch of edits, and before you declare work done, **delegate the
|
|
18
|
+
check-and-fix loop to the camas-fixer ladder** rather than running the checks and chasing
|
|
19
|
+
residuals in your own context. Subagents cannot nest — a haiku subagent cannot spawn a sonnet
|
|
20
|
+
one — so you orchestrate the escalation yourself:
|
|
21
|
+
|
|
22
|
+
- For a **lint/format** residual (the diagnostic names formatters or `--fix` linters): spawn
|
|
23
|
+
`camas-lint-fixer-haiku` with the changed paths as its scope. It takes one pass and hands back.
|
|
24
|
+
If it hands back not green, escalate by spawning `camas-lint-fixer-sonnet` on the same scope —
|
|
25
|
+
it also takes one pass. If *that* hands back not green, the residual is yours: take it from
|
|
26
|
+
there.
|
|
27
|
+
- For a **test/coverage** residual (the diagnostic names a test runner or coverage tool): spawn
|
|
28
|
+
`camas-test-fixer` directly — it fixes tests and coverage on a capable model in one delegation,
|
|
29
|
+
ending by re-gating to confirm green. If it hands back not green, or says the fix is ambiguous
|
|
30
|
+
(test wrong vs. behavior wrong), take it from there yourself.
|
|
31
|
+
- When diagnostics span both kinds, or you can't tell from the names, start with the lint ladder
|
|
32
|
+
(cheaper) and send whatever remains to `camas-test-fixer`.
|
|
33
|
+
|
|
34
|
+
Run each fixer in the background and keep working; for independent changed scopes, spawn one per
|
|
35
|
+
scope so they run in parallel. Each fixer hands back only what it could not settle: a green
|
|
36
|
+
result means that scope is done; a result quoting remaining diagnostics is the residual that
|
|
37
|
+
needs your reasoning.
|
|
38
|
+
|
|
39
|
+
**The Stop-hook nudge.** If your turn ends before you delegated (or a fixer's residual never got
|
|
40
|
+
picked up), a background check runs after you stop and, if the workspace isn't green, wakes you
|
|
41
|
+
with a reminder to launch the fixer ladder — so a skipped check-and-fix loop doesn't go silently
|
|
42
|
+
unnoticed. It wakes you at most once per prompt and never for a configuration gap (no check node
|
|
43
|
+
registered), so it cannot loop your turn. Prefer delegating proactively per the ladder above;
|
|
44
|
+
treat the nudge as a backstop, not your primary signal.
|
|
45
|
+
|
|
46
|
+
Never mask a residual — yours or a fixer's: do not suppress, disable, or loosen a check to make
|
|
47
|
+
the gate pass. A green gate must mean the work is actually correct.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
tags: ["*"]
|
|
7
|
+
pull_request:
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
|
|
13
|
+
concurrency:
|
|
14
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
15
|
+
cancel-in-progress: true
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
discover:
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
outputs:
|
|
21
|
+
axes: ${{ steps.axes.outputs.axes }}
|
|
22
|
+
tasks: ${{ steps.tasks.outputs.tasks }}
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
|
|
25
|
+
with:
|
|
26
|
+
fetch-depth: 0
|
|
27
|
+
persist-credentials: false
|
|
28
|
+
- uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
|
29
|
+
with:
|
|
30
|
+
enable-cache: false
|
|
31
|
+
|
|
32
|
+
- id: axes
|
|
33
|
+
run: echo "axes=$(uv run camas verify --github-matrix=axes)" >> "$GITHUB_OUTPUT"
|
|
34
|
+
- id: tasks
|
|
35
|
+
run: echo "tasks=$(uv run camas suite --github-matrix=tasks)" >> "$GITHUB_OUTPUT"
|
|
36
|
+
|
|
37
|
+
check:
|
|
38
|
+
needs: discover
|
|
39
|
+
strategy:
|
|
40
|
+
fail-fast: false
|
|
41
|
+
matrix:
|
|
42
|
+
os: [ubuntu-latest, ubuntu-24.04-arm, macos-latest, windows-latest]
|
|
43
|
+
task: ${{ fromJSON(needs.discover.outputs.tasks).task }}
|
|
44
|
+
python: ${{ fromJSON(needs.discover.outputs.axes).PY }}
|
|
45
|
+
runs-on: ${{ matrix.os }}
|
|
46
|
+
name: ${{ matrix.task }} · py${{ matrix.python }} · ${{ matrix.os }}
|
|
47
|
+
env:
|
|
48
|
+
UV_PYTHON: ${{ matrix.python }}
|
|
49
|
+
steps:
|
|
50
|
+
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
|
|
51
|
+
with:
|
|
52
|
+
fetch-depth: 0
|
|
53
|
+
persist-credentials: false
|
|
54
|
+
- uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
|
55
|
+
with:
|
|
56
|
+
enable-cache: false
|
|
57
|
+
- run: uv run camas "$TASK"
|
|
58
|
+
shell: bash
|
|
59
|
+
env:
|
|
60
|
+
TASK: ${{ matrix.task }}
|
|
61
|
+
|
|
62
|
+
build:
|
|
63
|
+
runs-on: ubuntu-latest
|
|
64
|
+
steps:
|
|
65
|
+
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
|
|
66
|
+
with:
|
|
67
|
+
fetch-depth: 0
|
|
68
|
+
persist-credentials: false
|
|
69
|
+
- uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
|
70
|
+
with:
|
|
71
|
+
enable-cache: false
|
|
72
|
+
- run: uv run camas build
|
|
73
|
+
- run: unzip -l dist/*.whl | grep -q types_bits/__init__.pyi
|
|
74
|
+
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
75
|
+
with:
|
|
76
|
+
name: dist
|
|
77
|
+
path: dist/
|
|
78
|
+
|
|
79
|
+
release:
|
|
80
|
+
if: startsWith(github.ref, 'refs/tags/')
|
|
81
|
+
needs: [check, build]
|
|
82
|
+
runs-on: ubuntu-latest
|
|
83
|
+
environment:
|
|
84
|
+
name: pypi
|
|
85
|
+
url: https://pypi.org/p/types-bits
|
|
86
|
+
permissions:
|
|
87
|
+
id-token: write
|
|
88
|
+
steps:
|
|
89
|
+
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
90
|
+
with:
|
|
91
|
+
name: dist
|
|
92
|
+
path: dist/
|
|
93
|
+
- uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
|
94
|
+
with:
|
|
95
|
+
enable-cache: false
|
|
96
|
+
- run: uv publish --trusted-publishing always
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Materialized type stubs — regenerate with `camas gen`
|
|
2
|
+
/src/types_bits/*.pyi
|
|
3
|
+
|
|
4
|
+
# Task runner cache
|
|
5
|
+
/.camas/
|
|
6
|
+
|
|
7
|
+
# Benchmark output
|
|
8
|
+
/bench/results/
|
|
9
|
+
|
|
10
|
+
.venv/
|
|
11
|
+
__pycache__/
|
|
12
|
+
*.py[cod]
|
|
13
|
+
dist/
|
|
14
|
+
.pytest_cache/
|
|
15
|
+
.mypy_cache/
|
|
16
|
+
.ruff_cache/
|
|
17
|
+
.pyrefly_cache/
|
|
18
|
+
.zuban_cache/
|
|
19
|
+
|
|
20
|
+
/bench/.work/
|
|
21
|
+
*.tmp
|
types_bits-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 JP Hutchins
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: types-bits
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Sized integer types: exhaustive Literal unions statically, Annotated bounds at runtime
|
|
5
|
+
Author-email: JP Hutchins <jphutchins@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.12
|
|
9
|
+
Provides-Extra: rt
|
|
10
|
+
Requires-Dist: annotated-types>=0.7; extra == 'rt'
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# types-bits
|
|
14
|
+
|
|
15
|
+
`u1`..`u10` and `i1`..`i10` as fully materialized `Literal` unions. Generated `.pyi`, zero
|
|
16
|
+
runtime cost, exhaustively checked.
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from typing import TYPE_CHECKING
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from types_bits import u4
|
|
23
|
+
|
|
24
|
+
x: u4 = 15 # ok
|
|
25
|
+
y: u4 = 16 # error, on every checker
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`camas` runs the gate; `camas --list` enumerates it. CI fans the same tree out over a
|
|
29
|
+
runner axis, with both matrix axes emitted from `tasks.py`. `camas bench` and
|
|
30
|
+
`camas bench_shapes` regenerate everything below into `bench/results/`.
|
|
31
|
+
|
|
32
|
+
## Runtime cost
|
|
33
|
+
|
|
34
|
+
Zero. The stub is a [PEP 484][pep484] `.pyi` behind `TYPE_CHECKING`. `import types_bits`
|
|
35
|
+
loads two modules and no stdlib (`tests/test_runtime.py`). Every number below is
|
|
36
|
+
type-checker wall clock, per check run.
|
|
37
|
+
|
|
38
|
+
## Check cost by width
|
|
39
|
+
|
|
40
|
+
Seconds, cold, best of one, Python 3.14.1 / WSL2. Encoding `flat`
|
|
41
|
+
(`type uN = Literal[0, ..., 2**N-1]`), declaring `u1..uN` (so a probe at N bits holds 2^(N+1)-2 literals) and widening each
|
|
42
|
+
into the next.
|
|
43
|
+
|
|
44
|
+
| bits | mypy | pyright | basedpyright | ty | pyrefly | zuban |
|
|
45
|
+
|---:|---:|---:|---:|---:|---:|---:|
|
|
46
|
+
| 8 | 2.30 | 0.68 | 0.71 | 0.08 | 0.32 | 0.10 |
|
|
47
|
+
| 10 | 1.98 | 0.75 | 0.83 | 0.09 | 0.38 | 0.12 |
|
|
48
|
+
| 12 | 2.22 | 0.79 | 0.85 | 0.11 | 0.31 | 0.33 |
|
|
49
|
+
| 14 | 3.10 | 0.59 | 0.64 | 0.17 | 0.27 | 3.23 |
|
|
50
|
+
| 16 | 3.85 | 0.70 | 0.79 | 0.41 | 0.46 | 51.26 |
|
|
51
|
+
|
|
52
|
+
Shipped library (`u1..u10` + `i1..i10`): mypy 2.05, basedpyright 1.43,
|
|
53
|
+
pyright 1.13, pyrefly 0.24, zuban 0.08, ty 0.06 — matching the `type uN = int` control.
|
|
54
|
+
|
|
55
|
+
```mermaid
|
|
56
|
+
xychart-beta
|
|
57
|
+
title "16 bits"
|
|
58
|
+
x-axis [zuban, mypy, basedpyright, pyright, pyrefly, ty]
|
|
59
|
+
y-axis "seconds" 0 --> 55
|
|
60
|
+
bar [51.26, 3.85, 0.79, 0.70, 0.46, 0.41]
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
zuban, same sweep — roughly 16x per extra 2 bits past 12:
|
|
64
|
+
|
|
65
|
+
```mermaid
|
|
66
|
+
xychart-beta
|
|
67
|
+
title "zuban vs width"
|
|
68
|
+
x-axis "bits" [8, 10, 12, 14, 16]
|
|
69
|
+
y-axis "seconds" 0 --> 55
|
|
70
|
+
line [0.10, 0.12, 0.33, 3.23, 51.26]
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The other five, 0–4s axis. Rising line is mypy; flat cluster is pyright, basedpyright,
|
|
74
|
+
pyrefly, ty:
|
|
75
|
+
|
|
76
|
+
```mermaid
|
|
77
|
+
xychart-beta
|
|
78
|
+
title "mypy, pyright, basedpyright, pyrefly, ty"
|
|
79
|
+
x-axis "bits" [8, 10, 12, 14, 16]
|
|
80
|
+
y-axis "seconds" 0 --> 4
|
|
81
|
+
line [2.30, 1.98, 2.22, 3.10, 3.85]
|
|
82
|
+
line [0.68, 0.75, 0.79, 0.59, 0.70]
|
|
83
|
+
line [0.71, 0.83, 0.85, 0.64, 0.79]
|
|
84
|
+
line [0.32, 0.38, 0.31, 0.27, 0.46]
|
|
85
|
+
line [0.08, 0.09, 0.11, 0.17, 0.41]
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Fixed vs marginal cost
|
|
89
|
+
|
|
90
|
+
`u(N-1)` and `u(N)` declared in every row; only the use count varies. Italic rows are the
|
|
91
|
+
`type uN = int` control.
|
|
92
|
+
|
|
93
|
+
10 bits — indistinguishable from `int`:
|
|
94
|
+
|
|
95
|
+
| uses | mypy | pyright | basedpyright | ty | pyrefly | zuban |
|
|
96
|
+
|---|---:|---:|---:|---:|---:|---:|
|
|
97
|
+
| declared, unused | 1.64 | 0.55 | 0.60 | 0.05 | 0.23 | 0.10 |
|
|
98
|
+
| *control* | *1.64* | *0.60* | *0.59* | *0.06* | *0.23* | *0.08* |
|
|
99
|
+
| 1 widening | 1.50 | 0.56 | 0.64 | 0.06 | 0.26 | 0.09 |
|
|
100
|
+
| 10 widenings | 1.75 | 0.65 | 0.90 | 0.08 | 0.29 | 0.20 |
|
|
101
|
+
|
|
102
|
+
16 bits — 98,304 literals:
|
|
103
|
+
|
|
104
|
+
| uses | mypy | pyright | basedpyright | ty | pyrefly | zuban |
|
|
105
|
+
|---|---:|---:|---:|---:|---:|---:|
|
|
106
|
+
| declared, unused | 2.79 | 0.66 | 0.64 | 0.18 | 0.29 | 0.20 |
|
|
107
|
+
| *control* | *1.57* | *0.62* | *0.59* | *0.05* | *0.28* | *0.08* |
|
|
108
|
+
| 1 assignment | 2.77 | 0.62 | 0.65 | 0.19 | 0.33 | 0.21 |
|
|
109
|
+
| 1 widening | 2.81 | 0.64 | 0.65 | 0.22 | 0.37 | **35.66** |
|
|
110
|
+
| 10 widenings | 3.31 | 0.56 | 0.58 | 0.20 | 0.31 | **timeout (>180s)** |
|
|
111
|
+
|
|
112
|
+
- Declaration: fixed per check run, ~linear in literals (~12 µs/literal on mypy). Not per
|
|
113
|
+
importing file, not per use.
|
|
114
|
+
- Assignment: free. Enumerated membership is a hash lookup.
|
|
115
|
+
- Widening: free except zuban at width.
|
|
116
|
+
|
|
117
|
+
## Widening
|
|
118
|
+
|
|
119
|
+
Cost tracks the *narrow* operand, not the wide one. Wide side fixed at `u16`, one widening:
|
|
120
|
+
|
|
121
|
+
| narrow | zuban | mypy | ty |
|
|
122
|
+
|---|---:|---:|---:|
|
|
123
|
+
| u1 | 0.18 | 2.54 | 0.20 |
|
|
124
|
+
| u4 | 0.18 | 2.46 | 0.15 |
|
|
125
|
+
| u8 | 0.16 | 2.67 | 0.21 |
|
|
126
|
+
| u12 | 0.76 | 2.50 | 0.17 |
|
|
127
|
+
| u15 | 35.21 | 2.80 | 0.18 |
|
|
128
|
+
|
|
129
|
+
Both operands must be large. At the 10-bit ceiling the worst case (`u9` → `u10`) is 0.09s.
|
|
130
|
+
Annotating a boundary at one width, so callers assign literals rather than widen between
|
|
131
|
+
adjacent wide aliases, avoids the shape entirely.
|
|
132
|
+
|
|
133
|
+
## Encodings
|
|
134
|
+
|
|
135
|
+
| encoding | form | result |
|
|
136
|
+
|---|---|---|
|
|
137
|
+
| `flat` | `Literal[0, ..., 2**N-1]` | shipped; fastest, portable |
|
|
138
|
+
| `nested` | `Literal[u9, 512, ...]` | legal per [PEP 586][pep586]; pyrefly rejects past ~10 levels (`Invalid type inside literal, int`, 1 error at 10 bits → 7 at 16), ty at 16 |
|
|
139
|
+
| `union` | `u9 \| Literal[512, ...]` | ty 44.24s at 14 bits vs 0.17s flat |
|
|
140
|
+
| `annotated` | `Annotated[Literal[...], Ge, Le]` | tracks `flat` within noise to 14 bits |
|
|
141
|
+
| `opaque` | `int` | control |
|
|
142
|
+
|
|
143
|
+
```mermaid
|
|
144
|
+
xychart-beta
|
|
145
|
+
title "ty at 14 bits by encoding"
|
|
146
|
+
x-axis [union, nested, annotated, flat]
|
|
147
|
+
y-axis "seconds" 0 --> 46
|
|
148
|
+
bar [44.24, 0.23, 0.22, 0.17]
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Runtime tier
|
|
152
|
+
|
|
153
|
+
[PEP 562][pep562] module `__getattr__` resolves the same names to
|
|
154
|
+
`Annotated[int, Ge(lo), Le(hi)]` ([PEP 593][pep593]), the shape
|
|
155
|
+
[`annotated-types`][at] consumers read. `tests/test_runtime.py` pins it against the static
|
|
156
|
+
bounds for all 20 widths. Needs the `rt` extra; `annotated_types` imports on first
|
|
157
|
+
attribute access.
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
from pydantic import TypeAdapter
|
|
161
|
+
from types_bits import u8 # Annotated[int, Ge(0), Le(255)] at runtime
|
|
162
|
+
|
|
163
|
+
TypeAdapter(u8).validate_python(256) # ValidationError
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Prior art
|
|
167
|
+
|
|
168
|
+
[`range-typed-integers`][rti] defines `u8 = NewType('u8', Annotated[int, ValueRange(0, 255)])`
|
|
169
|
+
for the byte widths `u8`..`u64` / `i8`..`i64`.
|
|
170
|
+
|
|
171
|
+
| | range-typed-integers | types-bits |
|
|
172
|
+
|---|---|---|
|
|
173
|
+
| carrier | `NewType` over `Annotated[int, ValueRange]` | `Literal` enumeration |
|
|
174
|
+
| bound enforced by | runtime `u8_checked()` / `check_int()`, raising `IntegerBoundError` | the type checker |
|
|
175
|
+
| `a: u8 = 12` | mypy error — `int` is not `u8`; requires `u8(12)` | ok |
|
|
176
|
+
| `a: u8 = 900` | mypy error, *identical* to the line above | error, and distinguished |
|
|
177
|
+
| `u8(900)` | accepted statically | n/a |
|
|
178
|
+
| widths | u8..u64 | u1..u10 |
|
|
179
|
+
|
|
180
|
+
Verified against every checker in the gate: mypy reports the same `Incompatible types in assignment
|
|
181
|
+
(expression has type "int", variable has type "u8")` on the in-range and out-of-range
|
|
182
|
+
lines alike. The range is metadata no checker reads ([PEP 746][pep746] would not change
|
|
183
|
+
this). `ValueRange` is O(1) per type, so it reaches u64; enumeration is O(2^N), so it
|
|
184
|
+
stops at u10.
|
|
185
|
+
|
|
186
|
+
## PEPs
|
|
187
|
+
|
|
188
|
+
No PEP provides bounded integers.
|
|
189
|
+
|
|
190
|
+
| PEP | Status | Relevance |
|
|
191
|
+
|---|---|---|
|
|
192
|
+
| [586 – Literal Types][pep586] | Final | The mechanism. Calls `Literal` insufficient for numpy-style numeric code and defers integer generics. Permits the `nested` form pyrefly rejects. |
|
|
193
|
+
| [593 – `Annotated`][pep593] | Final | Metadata channel for the runtime tier. |
|
|
194
|
+
| [695 – Type Parameter Syntax][pep695] | Final | `type uN = ...` in the stub. |
|
|
195
|
+
| [561 – Packaging Type Information][pep561] | Final | `py.typed`; why the stub ships in the wheel. |
|
|
196
|
+
| [562 – Module `__getattr__`][pep562] | Final | One name, two tiers. |
|
|
197
|
+
| [649][pep649] / [749][pep749] – Deferred Annotations | Final (3.14) | Guarded import gets cheaper. |
|
|
198
|
+
| [746 – Type checking `Annotated` metadata][pep746] | Draft, targets 3.15 | Lets a checker verify metadata suits its type. Does not make any checker enforce `Ge`/`Le`. |
|
|
199
|
+
|
|
200
|
+
- [python/typing#554, "Support for Range Types?"][i554] — closed. Requested an Ada-style
|
|
201
|
+
`RangeType`.
|
|
202
|
+
- [discuss.python.org, "Use type hinting with bound constraints, e.g. `int[0:15]`"][thread]
|
|
203
|
+
— no resolution. Objections: most constraints are not statically validatable; ranges need
|
|
204
|
+
new type-system machinery; arithmetic is not closed. Landed on `Annotated` +
|
|
205
|
+
`annotated-types` with runtime validation.
|
|
206
|
+
|
|
207
|
+
`fixtures/reject.py` covers the arithmetic case: every checker rejects `u4 + u4` where a
|
|
208
|
+
`u4` is required. mypy widens the sum to `int`; ty and zuban widen it to `Literal[0, ..., 30]`.
|
|
209
|
+
|
|
210
|
+
[pep484]: https://peps.python.org/pep-0484/
|
|
211
|
+
[pep561]: https://peps.python.org/pep-0561/
|
|
212
|
+
[pep562]: https://peps.python.org/pep-0562/
|
|
213
|
+
[pep586]: https://peps.python.org/pep-0586/
|
|
214
|
+
[pep593]: https://peps.python.org/pep-0593/
|
|
215
|
+
[pep649]: https://peps.python.org/pep-0649/
|
|
216
|
+
[pep695]: https://peps.python.org/pep-0695/
|
|
217
|
+
[pep746]: https://peps.python.org/pep-0746/
|
|
218
|
+
[pep749]: https://peps.python.org/pep-0749/
|
|
219
|
+
[at]: https://github.com/annotated-types/annotated-types
|
|
220
|
+
[i554]: https://github.com/python/typing/issues/554
|
|
221
|
+
[thread]: https://discuss.python.org/t/use-type-hinting-with-bound-constraints-e-g-int-0-15/38820
|
|
222
|
+
[rti]: https://github.com/theCapypara/range-typed-integers
|