Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion demo/use_case/fastapi_auto.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@
query,
)


# ──────────────────────────────────────────────────
# Service with FromContext
# ──────────────────────────────────────────────────
Expand Down
1 change: 0 additions & 1 deletion demo/use_case/mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
UseCaseService,
build_dto_select,
create_use_case_mcp_server,
mutation,
query,
)

Expand Down
12 changes: 6 additions & 6 deletions skill/template/src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@
Phase 3: + REST + MCP + Voyager with services
"""
from contextlib import asynccontextmanager
from typing import Any, Optional
from typing import Any

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import HTMLResponse, PlainTextResponse
from pydantic import BaseModel

from src.db import async_session
from sqlmodel_nexus import GraphQLHandler
from src.database import init_db
from src.db import async_session
from src.models import BaseEntity
from sqlmodel_nexus import GraphQLHandler

# ── GraphQL handler ───────────────────────────────────────────────────

Expand All @@ -27,8 +27,8 @@

# ── MCP apps (must be created before lifespan to combine lifespans) ───

from sqlmodel_nexus.mcp import create_mcp_server # noqa: E402
from sqlmodel_nexus import UseCaseAppConfig, create_use_case_mcp_server # noqa: E402
from sqlmodel_nexus.mcp import create_mcp_server # noqa: E402
from src.models import er # noqa: E402
from src.service.sprint.service import SprintService # noqa: E402
from src.service.task.service import TaskService # noqa: E402
Expand Down Expand Up @@ -101,8 +101,8 @@ async def lifespan(app: FastAPI):

class GraphQLRequest(BaseModel):
query: str
variables: Optional[dict[str, Any]] = None
operation_name: Optional[str] = None
variables: dict[str, Any] | None = None
operation_name: str | None = None


@app.get("/graphql", response_class=HTMLResponse)
Expand Down
1 change: 0 additions & 1 deletion skill/template/src/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
from sqlmodel import Field, Relationship, SQLModel, select

from sqlmodel_nexus import mutation, query

from src.db import async_session


Expand Down
1 change: 0 additions & 1 deletion skill/template/src/service/sprint/dtos.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""Sprint-related DTOs — SprintSummary with derived fields."""
from sqlmodel_nexus import DefineSubset, SubsetConfig

from src.models import Sprint
from src.service.task.dtos import TaskSummary

Expand Down
1 change: 0 additions & 1 deletion skill/template/src/service/sprint/service.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""Sprint UseCaseService — sprint management with task statistics."""
from sqlmodel_nexus import UseCaseService, build_dto_select, query

from src.database import async_session
from src.models import Resolver, Sprint
from src.service.sprint.dtos import SprintSummary
Expand Down
1 change: 0 additions & 1 deletion skill/template/src/service/task/dtos.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""Task-related DTOs — UserSummary, TaskSummary."""
from sqlmodel_nexus import DefineSubset, SubsetConfig

from src.models import Task, User


Expand Down
1 change: 0 additions & 1 deletion skill/template/src/service/task/service.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""Task UseCaseService — task management with auto-loaded owner."""
from sqlmodel_nexus import UseCaseService, build_dto_select, query

from src.database import async_session
from src.models import Resolver, Task
from src.service.task.dtos import TaskSummary
Expand Down
9 changes: 7 additions & 2 deletions src/sqlmodel_nexus/voyager/type_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,17 +285,22 @@ def safe_issubclass(kls, target_kls):
return False


def update_forward_refs(kls):
def update_forward_refs(kls, _visited: set | None = None):
"""Recursively update forward references in Pydantic models."""
if _visited is None:
_visited = set()
for shelled_type in get_core_types(kls):
if safe_issubclass(shelled_type, BaseModel):
if shelled_type in _visited:
continue
_visited.add(shelled_type)
try:
shelled_type.model_rebuild()
except Exception:
pass
# Recurse into fields
for field in shelled_type.model_fields.values():
update_forward_refs(field.annotation)
update_forward_refs(field.annotation, _visited)


def is_generic_container(cls):
Expand Down
1 change: 1 addition & 0 deletions src/sqlmodel_nexus/voyager/voyager_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from pathlib import Path

from sqlmodel_nexus.loader.registry import ErManager
from sqlmodel_nexus.use_case.business import UseCaseService
from sqlmodel_nexus.voyager.er_diagram_dot import ErDiagramDotBuilder
from sqlmodel_nexus.voyager.render import Renderer
from sqlmodel_nexus.voyager.render_style import DEFAULT_PRIMARY
Expand Down
2 changes: 1 addition & 1 deletion tests/test_argument_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ async def get_filtered(cls, limit: int):
registry = ErManager(entities=entities, session_factory=session_factory)
executor = QueryExecutor(registry)

method = getattr(UserQuery, "get_filtered")
method = UserQuery.get_filtered
query_methods = {"userGetFiltered": (FixtureUser, method)}
parsed = QueryParser().parse("{ userGetFiltered(limit: 1) { id name } }")

Expand Down
6 changes: 3 additions & 3 deletions tests/test_introspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def create_config(
score: float = 1.0,
is_active: bool = True,
tag: str | None = None,
tags: list[str] = [],
tags: list[str] | None = None,
) -> "IntrospectionConfig":
return cls(key=key, value=value)

Expand Down Expand Up @@ -697,12 +697,12 @@ def test_bool_false_default_format(self, generator: IntrospectionGenerator):
assert enabled_arg["defaultValue"] != "False"

def test_list_default_format(self, generator: IntrospectionGenerator):
"""List defaults must be valid GraphQL list literals."""
"""Optional list with None default renders as null in GraphQL."""
schema = generator.generate()
field = self._get_mutation_field(schema, "introspectionConfigCreateConfig")
tags_arg = next(a for a in field["args"] if a["name"] == "tags")

assert tags_arg["defaultValue"] == "[]"
assert tags_arg["defaultValue"] == "null"

def test_build_client_schema_succeeds(self, generator: IntrospectionGenerator):
"""Full introspection result must be consumable by graphql build_client_schema.
Expand Down
19 changes: 9 additions & 10 deletions tests/test_voyager_selfref.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,17 @@
3. Pass the DTO as a UseCaseService return type
4. Access Voyager endpoint → triggers update_forward_refs → RecursionError

Root cause: `update_forward_refs` has no visited-set, so self-referencing types
cause unbounded recursion through field annotations.
Root cause: `update_forward_refs` had no visited-set, so self-referencing types
caused unbounded recursion through field annotations.

Fix: Added a `_visited` set parameter to track already-processed types and
skip them on re-encounter, breaking the recursion cycle.
"""
from sqlmodel import Field, SQLModel

import pytest
from sqlmodel_nexus import DefineSubset, SubsetConfig
from sqlmodel_nexus.voyager.type_helper import update_forward_refs


# ── Minimal self-referencing setup ─────────────────────────────────────


Expand All @@ -37,10 +38,8 @@ class CommentDTO(DefineSubset):


class TestVoyagerSelfReference:
def test_update_forward_refs_on_self_referencing_dto_raises_recursion(self):
"""Directly calling update_forward_refs on a self-referencing DTO
triggers RecursionError. This documents the known bug — the fix
should add a visited-set to prevent re-processing seen types."""
def test_update_forward_refs_on_self_referencing_dto_completes(self):
"""update_forward_refs should handle self-referencing DTOs
without RecursionError by using a visited-set to avoid cycles."""

with pytest.raises(RecursionError):
update_forward_refs(CommentDTO)
update_forward_refs(CommentDTO) # should not raise
Loading