Skip to content

Commit 1e5e1b8

Browse files
Replace mypy with ty (#295)
1 parent d3ca730 commit 1e5e1b8

15 files changed

Lines changed: 108 additions & 197 deletions

File tree

‎.pre-commit-config.yaml‎

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,19 +31,14 @@ repos:
3131
files: ^backend/.*\.py$
3232

3333

34-
- repo: https://github.com/pre-commit/mirrors-mypy
35-
rev: v1.19.1
34+
- repo: local
3635
hooks:
37-
- id: mypy
38-
name: mypy (type checking)
39-
args:
40-
- --config-file=backend/pyproject.toml
41-
files: ^backend/.*\.py$
42-
additional_dependencies:
43-
- fastapi
44-
- pydantic
45-
- sqlalchemy
46-
- types-python-dateutil
36+
- id: ty
37+
name: ty (type checking)
38+
entry: bash -c 'cd backend && uv run ty check'
39+
language: system
40+
pass_filenames: false
41+
files: ^backend/app/.*\.py$
4742

4843
# ------------------------
4944
# Frontend (Node / TS)

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ Requests to `https://simboard-dev.e3sm.org/api/...` (the frontend origin) return
5858
- Backend: FastAPI, Pydantic, SQLAlchemy, Alembic
5959
- Database: PostgreSQL
6060
- Auth: GitHub OAuth for browser flows, API tokens for service accounts
61-
- Tooling: `uv`, `pnpm`, `ruff`, `mypy`, `eslint`, `prettier`, `pre-commit`
61+
- Tooling: `uv`, `pnpm`, `ruff`, `ty`, `eslint`, `prettier`, `pre-commit`
6262
- CI/CD: GitHub Actions plus NERSC-focused deployment/build docs under `docs/`
6363

6464
## Documentation Map

‎backend/.pre-commit-config.yaml‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ default_stages: [pre-commit]
33
fail_fast: true
44

55
# To update pre-commit hooks, run `pre-commit autoupdate` in the terminal.
6-
# Then update the versions in `conda-env/dev.yml`.
76
repos:
87
- repo: https://github.com/pre-commit/pre-commit-hooks
98
rev: v6.0.0
@@ -25,9 +24,12 @@ repos:
2524
# Run the formatter.
2625
- id: ruff-format
2726

28-
- repo: https://github.com/pre-commit/mirrors-mypy
29-
rev: v1.18.2
27+
- repo: local
3028
hooks:
31-
- id: mypy
32-
args: ["--config=backend/pyproject.toml"]
33-
additional_dependencies: [types-requests, pydantic, types-python-dateutil]
29+
- id: ty
30+
name: ty (type checking)
31+
entry: >
32+
bash -c 'cd "$(git rev-parse --show-toplevel)/backend" && uv run ty check'
33+
language: system
34+
pass_filenames: false
35+
files: ^(?:backend/)?app/.*\.py$

‎backend/app/features/assistant/llm_generator.py‎

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -114,24 +114,25 @@ def _build_model(self, http_client: AsyncClient) -> OpenAIChatModel:
114114
if self.config.provider == "ollama":
115115
api_key = api_key or _OLLAMA_PLACEHOLDER_API_KEY
116116

117-
provider_kwargs = {
118-
"api_key": api_key,
119-
"base_url": self._resolve_base_url(),
120-
"http_client": http_client,
121-
}
122117
if self.config.provider == "ollama":
123-
provider_kwargs = {
124-
"openai_client": AsyncOpenAI(
125-
**provider_kwargs,
118+
provider = OpenAIProvider(
119+
openai_client=AsyncOpenAI(
120+
api_key=api_key,
121+
base_url=self._resolve_base_url(),
122+
http_client=http_client,
126123
max_retries=0,
127124
timeout=self.config.timeout_seconds,
128125
_enforce_credentials=False,
129126
)
130-
}
127+
)
128+
else:
129+
provider = OpenAIProvider(
130+
api_key=api_key,
131+
base_url=self._resolve_base_url(),
132+
http_client=http_client,
133+
)
131134

132-
model = OpenAIChatModel(
133-
self.config.model_name, provider=OpenAIProvider(**provider_kwargs)
134-
)
135+
model = OpenAIChatModel(self.config.model_name, provider=provider)
135136
return model
136137

137138
def _resolve_base_url(self) -> str | None:

‎backend/app/features/catalog/api.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1191,7 +1191,7 @@ def _machine_filter_options(db: Session) -> list[FilterOptionOut]:
11911191
def _creator_filter_options(db: Session) -> list[FilterOptionOut]:
11921192
"""Return execution creators with stable IDs and email labels."""
11931193
rows = (
1194-
db.query(User.id, User.email)
1194+
db.query(User.id, User.email) # ty: ignore[no-matching-overload] -- SQLAlchemy mapped descriptors are not recognized as query columns.
11951195
.join(Execution, Execution.created_by == User.id)
11961196
.distinct()
11971197
.order_by(User.email, User.id)

‎backend/app/features/user/api/oauth.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ async def github_callback(
185185
if has_verified_e3sm_membership is not None and hasattr(
186186
user_manager, "refresh_github_org_membership"
187187
):
188-
user = await user_manager.refresh_github_org_membership(
188+
user = await user_manager.refresh_github_org_membership( # ty: ignore[call-non-callable] -- FastAPI Users' untyped extension method is narrowed by hasattr.
189189
user,
190190
is_verified_member=has_verified_e3sm_membership,
191191
checked_at=datetime.now(timezone.utc),

‎backend/app/features/user/api/token.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def create_api_token(
7474
detail="Only administrators can create API tokens",
7575
)
7676

77-
stmt = select(User).where(User.id == payload.user_id)
77+
stmt = select(User).where(User.id == payload.user_id) # ty: ignore[invalid-argument-type] -- SQLAlchemy mapped descriptor comparison produces a SQL expression at runtime.
7878
result = db.execute(stmt)
7979
target_user = result.scalar_one_or_none()
8080

@@ -259,7 +259,7 @@ def create_service_account(
259259

260260
email = f"{payload.service_name}@{settings.domain}"
261261

262-
existing = db.query(User).filter(User.email == email).first()
262+
existing = db.query(User).filter(User.email == email).first() # ty: ignore[invalid-argument-type] -- SQLAlchemy mapped descriptor comparison produces a SQL expression at runtime.
263263
if existing is not None:
264264
return ServiceAccountResponse(
265265
id=existing.id,

‎backend/app/features/user/auth/token.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ def validate_token(
9797
if datetime.now(timezone.utc) > token.expires_at:
9898
return None
9999

100-
user = db.query(User).filter(User.id == token.user_id).first()
100+
user = db.query(User).filter(User.id == token.user_id).first() # ty: ignore[invalid-argument-type] -- SQLAlchemy mapped descriptor comparison produces a SQL expression at runtime.
101101

102102
if not user or not user.is_active:
103103
return None

‎backend/app/features/user/manager.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@
1919

2020

2121
async def get_user_db(session: AsyncSession = Depends(get_async_session)): # noqa: B008
22-
yield SQLAlchemyUserDatabase(session, User, OAuthAccount)
22+
yield SQLAlchemyUserDatabase(session, User, OAuthAccount) # ty: ignore[invalid-argument-type] -- FastAPI Users' protocol does not support nullable OAuth passwords.
2323

2424

25-
class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
25+
class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]): # ty: ignore[invalid-type-arguments] -- OAuth users intentionally have nullable passwords.
2626
async def on_after_register(self, user: User, request=None):
2727
logger.info(f"✅ New GitHub user registered: {user.email}")
2828

@@ -48,7 +48,7 @@ async def get_user_manager(user_db=Depends(get_user_db)): # noqa: B008
4848
yield UserManager(user_db)
4949

5050

51-
fastapi_users = FastAPIUsers[User, uuid.UUID](
51+
fastapi_users = FastAPIUsers[User, uuid.UUID]( # ty: ignore[invalid-type-arguments] -- OAuth users intentionally have nullable passwords.
5252
get_user_manager, [GITHUB_OAUTH_BACKEND, JWT_BEARER_BACKEND]
5353
)
5454

‎backend/app/scripts/db/rollback_seed.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ def rollback_seed(db: Session):
119119

120120
# Remove only the dummy OAuth account/user created by seed.py
121121
dev_user_id = db.execute(
122-
select(User.id).where(User.__table__.c.email == DEV_EMAIL)
122+
select(User.id).where(User.__table__.c.email == DEV_EMAIL) # ty: ignore[no-matching-overload] -- SQLAlchemy declarative table columns are dynamically generated.
123123
).scalar_one_or_none()
124124

125125
if dev_user_id:

0 commit comments

Comments
 (0)