237 lines
8.5 KiB
Python
237 lines
8.5 KiB
Python
"""
|
|
Graphene Generator
|
|
|
|
Generates graphene ObjectType and InputObjectType classes from model definitions.
|
|
Only generates type definitions — queries, mutations, and resolvers are hand-written.
|
|
"""
|
|
|
|
from enum import Enum
|
|
from pathlib import Path
|
|
from typing import Any, List, get_type_hints
|
|
|
|
from ..helpers import get_origin_name, get_type_name, unwrap_optional
|
|
from ..loader.schema import EnumDefinition, FieldDefinition, ModelDefinition
|
|
from ..types import GRAPHENE_RESOLVERS
|
|
from .base import BaseGenerator
|
|
|
|
|
|
class GrapheneGenerator(BaseGenerator):
|
|
"""Generates graphene type definition files."""
|
|
|
|
def file_extension(self) -> str:
|
|
return ".py"
|
|
|
|
def generate(self, models, output_path: Path) -> None:
|
|
"""Generate graphene types to output_path."""
|
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
if hasattr(models, "models"):
|
|
# SchemaLoader
|
|
content = self._generate_from_definitions(
|
|
models.models,
|
|
getattr(models, "enums", []),
|
|
getattr(models, "api_models", []),
|
|
)
|
|
elif isinstance(models, tuple):
|
|
content = self._generate_from_definitions(models[0], models[1], [])
|
|
elif isinstance(models, list):
|
|
content = self._generate_from_dataclasses(models)
|
|
else:
|
|
raise ValueError(f"Unsupported input type: {type(models)}")
|
|
|
|
output_path.write_text(content)
|
|
|
|
def _generate_from_definitions(
|
|
self,
|
|
models: List[ModelDefinition],
|
|
enums: List[EnumDefinition],
|
|
api_models: List[ModelDefinition],
|
|
) -> str:
|
|
"""Generate from ModelDefinition objects."""
|
|
lines = self._generate_header()
|
|
|
|
# Generate enums as graphene.Enum
|
|
for enum_def in enums:
|
|
lines.extend(self._generate_enum(enum_def))
|
|
lines.append("")
|
|
lines.append("")
|
|
|
|
# Generate domain models as ObjectType
|
|
for model_def in models:
|
|
lines.extend(self._generate_object_type(model_def))
|
|
lines.append("")
|
|
lines.append("")
|
|
|
|
# Generate API models — request types as InputObjectType, others as ObjectType
|
|
for model_def in api_models:
|
|
if model_def.name.endswith("Request"):
|
|
lines.extend(self._generate_input_type(model_def))
|
|
else:
|
|
lines.extend(self._generate_object_type(model_def))
|
|
lines.append("")
|
|
lines.append("")
|
|
|
|
return "\n".join(lines).rstrip() + "\n"
|
|
|
|
def _generate_from_dataclasses(self, dataclasses: List[type]) -> str:
|
|
"""Generate from Python dataclasses."""
|
|
lines = self._generate_header()
|
|
|
|
enums_generated = set()
|
|
for cls in dataclasses:
|
|
hints = get_type_hints(cls)
|
|
for type_hint in hints.values():
|
|
base, _ = unwrap_optional(type_hint)
|
|
if isinstance(base, type) and issubclass(base, Enum):
|
|
if base.__name__ not in enums_generated:
|
|
lines.extend(self._generate_enum_from_python(base))
|
|
lines.append("")
|
|
lines.append("")
|
|
enums_generated.add(base.__name__)
|
|
|
|
for cls in dataclasses:
|
|
lines.extend(self._generate_object_type_from_dataclass(cls))
|
|
lines.append("")
|
|
lines.append("")
|
|
|
|
return "\n".join(lines).rstrip() + "\n"
|
|
|
|
def _generate_header(self) -> List[str]:
|
|
return [
|
|
'"""',
|
|
"Graphene Types - GENERATED FILE",
|
|
"",
|
|
"Do not edit directly. Regenerate using modelgen.",
|
|
'"""',
|
|
"",
|
|
"import graphene",
|
|
"",
|
|
"",
|
|
]
|
|
|
|
def _generate_enum(self, enum_def: EnumDefinition) -> List[str]:
|
|
"""Generate graphene.Enum from EnumDefinition."""
|
|
lines = [f"class {enum_def.name}(graphene.Enum):"]
|
|
for name, value in enum_def.values:
|
|
lines.append(f' {name} = "{value}"')
|
|
return lines
|
|
|
|
def _generate_enum_from_python(self, enum_cls: type) -> List[str]:
|
|
"""Generate graphene.Enum from Python Enum."""
|
|
lines = [f"class {enum_cls.__name__}(graphene.Enum):"]
|
|
for member in enum_cls:
|
|
lines.append(f' {member.name} = "{member.value}"')
|
|
return lines
|
|
|
|
def _generate_object_type(self, model_def: ModelDefinition) -> List[str]:
|
|
"""Generate graphene.ObjectType from ModelDefinition."""
|
|
name = model_def.name
|
|
# Append Type suffix if not already present
|
|
type_name = f"{name}Type" if not name.endswith("Type") else name
|
|
|
|
lines = [f"class {type_name}(graphene.ObjectType):"]
|
|
if model_def.docstring:
|
|
doc = model_def.docstring.strip().split("\n")[0]
|
|
lines.append(f' """{doc}"""')
|
|
lines.append("")
|
|
|
|
if not model_def.fields:
|
|
lines.append(" pass")
|
|
else:
|
|
for field in model_def.fields:
|
|
graphene_type = self._resolve_type(field.type_hint, field.optional)
|
|
lines.append(f" {field.name} = {graphene_type}")
|
|
|
|
return lines
|
|
|
|
def _generate_input_type(self, model_def: ModelDefinition) -> List[str]:
|
|
"""Generate graphene.InputObjectType from ModelDefinition."""
|
|
import dataclasses as dc
|
|
|
|
name = model_def.name
|
|
# Convert FooRequest -> FooInput
|
|
if name.endswith("Request"):
|
|
input_name = name[: -len("Request")] + "Input"
|
|
else:
|
|
input_name = f"{name}Input"
|
|
|
|
lines = [f"class {input_name}(graphene.InputObjectType):"]
|
|
if model_def.docstring:
|
|
doc = model_def.docstring.strip().split("\n")[0]
|
|
lines.append(f' """{doc}"""')
|
|
lines.append("")
|
|
|
|
if not model_def.fields:
|
|
lines.append(" pass")
|
|
else:
|
|
for field in model_def.fields:
|
|
graphene_type = self._resolve_type(field.type_hint, field.optional)
|
|
# Required only if not optional AND no default value
|
|
has_default = field.default is not dc.MISSING
|
|
if not field.optional and not has_default:
|
|
graphene_type = self._make_required(graphene_type)
|
|
elif has_default and not field.optional:
|
|
graphene_type = self._add_default(graphene_type, field.default)
|
|
lines.append(f" {field.name} = {graphene_type}")
|
|
|
|
return lines
|
|
|
|
def _generate_object_type_from_dataclass(self, cls: type) -> List[str]:
|
|
"""Generate graphene.ObjectType from a dataclass."""
|
|
import dataclasses as dc
|
|
|
|
type_name = f"{cls.__name__}Type"
|
|
lines = [f"class {type_name}(graphene.ObjectType):"]
|
|
|
|
hints = get_type_hints(cls)
|
|
for name, type_hint in hints.items():
|
|
if name.startswith("_"):
|
|
continue
|
|
graphene_type = self._resolve_type(type_hint, False)
|
|
lines.append(f" {name} = {graphene_type}")
|
|
|
|
return lines
|
|
|
|
def _resolve_type(self, type_hint: Any, optional: bool) -> str:
|
|
"""Resolve Python type to graphene field call string."""
|
|
base, is_optional = unwrap_optional(type_hint)
|
|
optional = optional or is_optional
|
|
origin = get_origin_name(base)
|
|
type_name = get_type_name(base)
|
|
|
|
# Look up resolver
|
|
resolver = (
|
|
GRAPHENE_RESOLVERS.get(origin)
|
|
or GRAPHENE_RESOLVERS.get(type_name)
|
|
or GRAPHENE_RESOLVERS.get(base)
|
|
or (
|
|
GRAPHENE_RESOLVERS["enum"]
|
|
if isinstance(base, type) and issubclass(base, Enum)
|
|
else None
|
|
)
|
|
)
|
|
|
|
result = resolver(base) if resolver else "graphene.String"
|
|
|
|
# List types already have () syntax from resolver
|
|
if result.startswith("graphene.List("):
|
|
return result
|
|
|
|
# Scalar types: add () call
|
|
return f"{result}()"
|
|
|
|
def _make_required(self, field_str: str) -> str:
|
|
"""Add required=True to a graphene field."""
|
|
if field_str.endswith("()"):
|
|
return field_str[:-1] + "required=True)"
|
|
return field_str
|
|
|
|
def _add_default(self, field_str: str, default: Any) -> str:
|
|
"""Add default_value to a graphene field."""
|
|
if callable(default):
|
|
# default_factory — skip, graphene doesn't support factories
|
|
return field_str
|
|
if field_str.endswith("()"):
|
|
return field_str[:-1] + f"default_value={default!r})"
|
|
return field_str
|