Skip to content

Commit 28b64e0

Browse files
authored
Refactor/01 typing gate and tooling (#52)
* docs: add refactor ticket plan (plans/refactor, tickets 01-07) * refactor(tooling): add ty type-check gate, broaden ruff, prune unused deps Implements plans/refactor/01-typing-gate-and-tooling.md: - Add ty (pinned 0.0.58) as a blocking type-check gate in CI, pre-commit, and 'make typecheck', with a per-path ratchet in [tool.ty.src] exclude; each exclusion is owned by a refactor ticket that removes it - Broaden ruff to F,E,W,I,UP,B,SIM,RUF; drop E721/F841 ignores; remove stale src/codesphere_sdk excludes; fix all resulting violations (pyupgrade modernizations, import sorting, PEP 695 type aliases, implicit-Optional fixes, exception chaining) - Fix coverage config: source was nonexistent packages [api,handler,tasks], now src/codesphere; CI uses --cov from config instead of --cov=. - Remove unused runtime deps: aiohttp, aiohttp-retry, urllib3, python-dateutil, typing-extensions (verified zero imports) - Relax requires-python from >=3.12.9 to >=3.12 - Trigger CI on pyproject.toml/ruff.toml/uv.lock changes
1 parent ae3d6f2 commit 28b64e0

72 files changed

Lines changed: 1216 additions & 842 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,41 @@ on:
77
- 'src/codesphere/**'
88
- '.github/workflows/ci.yml'
99
- 'tests/**'
10+
- 'pyproject.toml'
11+
- 'ruff.toml'
12+
- 'uv.lock'
1013

1114
permissions:
1215
contents: write
1316
pull-requests: write
1417

1518
jobs:
19+
typecheck:
20+
name: Type Check (ty)
21+
runs-on: ubuntu-latest
22+
permissions:
23+
contents: read
24+
25+
steps:
26+
- name: Checkout repository
27+
uses: actions/checkout@v4
28+
29+
- name: Install uv package manager
30+
uses: astral-sh/setup-uv@v6
31+
with:
32+
activate-environment: true
33+
34+
- name: Install dependencies
35+
run: uv sync --extra dev
36+
shell: bash
37+
38+
- name: Run ty type check
39+
run: uv run ty check
40+
shell: bash
41+
42+
- name: Minimize uv cache
43+
run: uv cache prune --ci
44+
1645
security_check:
1746
name: Security Check (Bandit)
1847
runs-on: ubuntu-latest
@@ -123,7 +152,7 @@ jobs:
123152

124153
- name: Run tests with pytest
125154
run: |
126-
uv run pytest --junitxml=junit/test-results.xml --cov-report=xml --cov-report=html --cov=. --ignore=tests/integration | tee pytest-coverage.txt
155+
uv run pytest --junitxml=junit/test-results.xml --cov-report=xml --cov-report=html --cov --ignore=tests/integration | tee pytest-coverage.txt
127156
shell: bash
128157

129158
- name: Pytest coverage comment

.gitignore

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,8 @@ env/
2020

2121
.pytest_cache
2222

23-
__marimo__
23+
__marimo__
24+
# Coverage artifacts
25+
.coverage
26+
coverage.xml
27+
test-results/

.pre-commit-config.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,12 @@ repos:
1111
- id: ruff-check
1212
args: [ --fix ]
1313
- id: ruff-format
14+
15+
- repo: local
16+
hooks:
17+
- id: ty-check
18+
name: ty check
19+
entry: uv run ty check
20+
language: system
21+
types: [python]
22+
pass_filenames: false

Makefile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: help install lint format test test-integration test-unit bump release pypi tree version changelog
1+
.PHONY: help install lint format typecheck test test-integration test-unit bump release pypi tree version changelog
22

33
.DEFAULT_GOAL := help
44

@@ -27,6 +27,10 @@ format: ## Formats code with ruff
2727
@echo ">>> Formatting code with ruff..."
2828
uv run ruff format src
2929

30+
typecheck: ## Checks types with ty
31+
@echo ">>> Checking types with ty..."
32+
uv run ty check
33+
3034
test: ## Runs all tests with pytest
3135
@echo ">>> Running all tests with pytest..."
3236
uv run pytest

examples/dashboard/app.py

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,8 @@
1717

1818
import asyncio
1919
from contextlib import asynccontextmanager
20-
from datetime import datetime, timedelta, timezone
20+
from datetime import UTC, datetime, timedelta
2121
from pathlib import Path
22-
from typing import Optional
2322

2423
from dotenv import load_dotenv
2524
from fastapi import FastAPI, Form, HTTPException, Query, Request
@@ -44,7 +43,7 @@
4443
from codesphere.resources.workspace.logs import LogStage # noqa: E402
4544

4645
# Global SDK instance (managed via lifespan)
47-
cs: Optional[CodesphereSDK] = None
46+
cs: CodesphereSDK | None = None
4847

4948

5049
@asynccontextmanager
@@ -71,7 +70,7 @@ async def lifespan(app: FastAPI):
7170
# =============================================================================
7271

7372

74-
def format_datetime(value: Optional[datetime]) -> str:
73+
def format_datetime(value: datetime | None) -> str:
7574
"""Format datetime for display."""
7675
if value is None:
7776
return "N/A"
@@ -156,7 +155,7 @@ async def team_detail(request: Request, team_id: int):
156155

157156
# Get usage summary (last 7 days)
158157
try:
159-
end_date = datetime.now(timezone.utc)
158+
end_date = datetime.now(UTC)
160159
begin_date = end_date - timedelta(days=7)
161160
usage = await team.usage.get_landscape_summary(
162161
begin_date=begin_date, end_date=end_date, limit=10
@@ -243,8 +242,8 @@ async def create_workspace(
243242
team_id: int = Form(...),
244243
name: str = Form(...),
245244
plan_id: int = Form(...),
246-
base_image: Optional[str] = Form(None),
247-
git_url: Optional[str] = Form(None),
245+
base_image: str | None = Form(None),
246+
git_url: str | None = Form(None),
248247
):
249248
"""Create a new workspace."""
250249
try:
@@ -265,16 +264,16 @@ async def create_workspace(
265264
"partials/error.html",
266265
{"request": request, "error": str(e)},
267266
)
268-
raise HTTPException(status_code=400, detail=str(e))
267+
raise HTTPException(status_code=400, detail=str(e)) from e
269268

270269

271270
@app.post("/workspaces/{workspace_id}/update", response_class=HTMLResponse)
272271
async def update_workspace(
273272
request: Request,
274273
workspace_id: int,
275-
name: Optional[str] = Form(None),
276-
plan_id: Optional[int] = Form(None),
277-
replicas: Optional[int] = Form(None),
274+
name: str | None = Form(None),
275+
plan_id: int | None = Form(None),
276+
replicas: int | None = Form(None),
278277
):
279278
"""Update workspace settings."""
280279
workspace = await cs.workspaces.get(workspace_id=workspace_id)
@@ -630,8 +629,8 @@ async def logs_partial(
630629
workspace_id: int,
631630
stage: str = Query(default="prepare"),
632631
step: int = Query(default=0),
633-
server: Optional[str] = Query(default=None),
634-
replica: Optional[int] = Query(default=None),
632+
server: str | None = Query(default=None),
633+
replica: int | None = Query(default=None),
635634
):
636635
"""Get logs for a pipeline stage or server."""
637636
workspace = await cs.workspaces.get(workspace_id=workspace_id)

examples/scripts/create_workspace_with_landscape.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import asyncio
22
import time
3-
from datetime import datetime, timedelta, timezone
3+
from datetime import UTC, datetime, timedelta
44

55
from codesphere import CodesphereSDK
66
from codesphere.resources.workspace import WorkspaceCreate
@@ -44,7 +44,8 @@ async def main():
4444
.add_reactive_service("web")
4545
.plan(plan.id)
4646
.add_step(
47-
'for i in $(seq 1 20); do echo "[$i] Processing request..."; sleep 1; done'
47+
"for i in $(seq 1 20); do "
48+
'echo "[$i] Processing request..."; sleep 1; done'
4849
)
4950
.add_port(3000, public=True)
5051
.add_path("/", port=3000)
@@ -95,11 +96,12 @@ async def main():
9596

9697
print("\n--- Usage History ---")
9798

98-
end_date = datetime.now(timezone.utc)
99+
end_date = datetime.now(UTC)
99100
begin_date = end_date - timedelta(days=1)
100101

101102
print(
102-
f"Fetching usage summary from {begin_date.isoformat()} to {end_date.isoformat()}..."
103+
f"Fetching usage summary from {begin_date.isoformat()} "
104+
f"to {end_date.isoformat()}..."
103105
)
104106
usage_summary = await team.usage.get_landscape_summary(
105107
begin_date=begin_date,
@@ -135,7 +137,8 @@ async def main():
135137
print(f"Total events: {events.total_items}")
136138
for event in events.items:
137139
print(
138-
f" [{event.date.isoformat()}] {event.action.value.upper()} by {event.initiator_email}"
140+
f" [{event.date.isoformat()}] {event.action.value.upper()} "
141+
f"by {event.initiator_email}"
139142
)
140143

141144
print("\nRefreshing usage summary...")

examples/sdk_demo.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
with app.setup:
1515
"""Import dependencies and initialize the SDK."""
1616
import os
17+
1718
from codesphere import CodesphereSDK
1819

1920
# Display token status
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# 01 — Add a blocking `ty` type-check gate (ratcheted) and clean up tooling config
2+
3+
**Priority:** P1
4+
**Depends on:**
5+
**Unblocks:** 02–07 (the ratchet is how later tickets prove their typing work)
6+
7+
## Problem
8+
9+
The SDK ships `src/codesphere/py.typed` (so downstream type checkers trust our annotations) and
10+
`.github/copilot-instructions.md` states "Strict type hints required" — but **no type checker
11+
exists anywhere**: not in dev dependencies, not in `.pre-commit-config.yaml`, not in
12+
`.github/workflows/ci.yml`. The claim is unenforced, and several annotations in `core/` are
13+
actively wrong (see ticket 02).
14+
15+
Surrounding tooling config has drifted:
16+
17+
- `ruff.toml` selects only `["F", "E4", "E7", "E9"]` and ignores `E721`/`F841` (unused locals
18+
pass lint). Its `exclude` list references `src/codesphere_sdk/...` paths that don't exist —
19+
leftovers from a generated-client era.
20+
- `pyproject.toml` `[tool.coverage.run]` has `source = ["api", "handler", "tasks"]`
21+
(pyproject.toml:64) — none of those packages exist, so coverage numbers are unanchored.
22+
`omit` also lists nonexistent `docs/*` / `scripts/**` roots.
23+
- `pyproject.toml:13-14,22` declare `aiohttp`, `aiohttp-retry`, and `urllib3` as runtime
24+
dependencies; `grep -r` over `src/` shows zero imports of any of them (the SDK is pure httpx).
25+
They bloat installs and imply a retry feature that doesn't exist (see ticket 07).
26+
- `requires-python = ">=3.12.9"` (pyproject.toml:11) pins an oddly specific patch release,
27+
excluding 3.12.0–3.12.8 users for no identified reason.
28+
29+
## Approach
30+
31+
Toolchain choice: **`ty`** (Astral) rather than mypy/pyright, to keep type checking and linting
32+
in the same toolchain family as ruff. ty is pre-1.0: **pin its version** in the dev group and
33+
expect occasional rule renames on upgrades (note this in a comment next to the pin).
34+
35+
1. **Add ty as a blocking gate with a ratchet.**
36+
- `uv add --dev ty` (pinned, e.g. `ty==0.0.x`).
37+
- Configure in `pyproject.toml` under `[tool.ty]`: `src.root = "src"`, error-level rules for
38+
the strictness-relevant checks (unresolved attributes, invalid assignments, invalid
39+
argument types, missing/implicit `Any` where ty supports it).
40+
- **Ratchet:** exclude the paths that cannot pass until later tickets land, via
41+
`[[tool.ty.overrides]]` (or `src.exclude` if the pinned ty version's override granularity
42+
is insufficient):
43+
- `src/codesphere/core/**` and `src/codesphere/resources/**` → removed by tickets 02/04
44+
- `src/codesphere/http_client.py`, `src/codesphere/client.py`, `src/codesphere/config.py` → removed by ticket 03
45+
Annotate each exclusion with the ticket number that deletes it.
46+
- CI: add a `typecheck` job to `.github/workflows/ci.yml` running `uv run ty check`
47+
(blocking). Add the same to `.pre-commit-config.yaml` and a `make typecheck` target.
48+
49+
2. **Broaden ruff.**
50+
- `select = ["F", "E", "W", "I", "UP", "B", "SIM", "RUF"]`; drop the `F841` and `E721`
51+
ignores; delete the entire stale `src/codesphere_sdk/*` exclude block.
52+
- Run `ruff check --fix` + `ruff format`; fix the residue by hand (expect mostly import
53+
sorting and pyupgrade rewrites like `Optional[X]``X | None`).
54+
55+
3. **Fix coverage config.** `[tool.coverage.run] source = ["src/codesphere"]`; prune the
56+
nonexistent `omit` entries; change the CI pytest invocation from `--cov=.` to rely on the
57+
config (`--cov`).
58+
59+
4. **Prune dependencies.** Remove `aiohttp`, `aiohttp-retry`, `urllib3` from
60+
`[project.dependencies]`. While there, grep-verify each remaining runtime dep is actually
61+
imported (`python-dateutil` is suspect) and remove any that aren't. `uv lock` after.
62+
63+
5. **Relax the Python floor** to `requires-python = ">=3.12"` (align `.python-version` and
64+
`ruff.toml target-version = "py312"`, which already agree).
65+
66+
## Breaking changes
67+
68+
None. Dependency removals only shrink the install footprint; relaxing `requires-python` widens
69+
compatibility.
70+
71+
## Acceptance criteria
72+
73+
- [ ] CI fails on any ty error outside the documented ratchet exclusions; `make typecheck` and
74+
the pre-commit hook run the same command.
75+
- [ ] Every ratchet exclusion carries a comment naming the ticket that removes it.
76+
- [ ] `ruff check` passes with the broadened rule set; no `codesphere_sdk` references remain in
77+
`ruff.toml`.
78+
- [ ] Coverage output reports lines in `src/codesphere/**` (spot-check the CI coverage comment).
79+
- [ ] Fresh `uv pip install .` pulls neither aiohttp nor urllib3; `uv.lock` regenerated.
80+
- [ ] `requires-python = ">=3.12"`.

0 commit comments

Comments
 (0)