""" 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