Developer Guide#
This guide explains how the Cannula code generation system works internally for developers who want to modify or extend the code generation capabilities.
Architecture Overview#
The code generation system consists of several key components:
- Schema Analysis Layer (schema_analyzer.py)
Processes GraphQL schema and metadata
Creates intermediate representations
Handles relationships and forward references
- Code Generation Layer
Base CodeGenerator class
- Specialized generators for different outputs:
PythonCodeGenerator for type definitions
SQLAlchemyGenerator for database models
ContextGenerator for data source classes
- Parsing Layer
parse_type.py - GraphQL type parsing
parse_args.py - Argument parsing
Data Flow#
GraphQL Schema + Metadata
↓
SchemaAnalyzer
↓
ObjectTypes, Fields, etc.
↓
CodeGenerators
↓
AST Generation
↓
Final Code
Schema Analysis#
The SchemaAnalyzer class is the entry point for processing schemas:
class SchemaAnalyzer:
def __init__(self, schema: GraphQLSchema):
self.schema = schema
self.extensions = SchemaExtension(schema)
self._analyze()
Key responsibilities:
Categorizing types (objects, interfaces, unions, etc.)
Processing metadata
Handling relationships and forward references
Creating intermediate representations
Type System#
The system uses several intermediate representations:
- ObjectType
Represents GraphQL object types
Holds fields and metadata
Tracks relationships
- Field
Represents GraphQL fields
Contains type information
Holds arguments and metadata
- FieldType
Represents field types
Handles lists and nullability
Manages type references
Code Generation Base#
The CodeGenerator base class provides common functionality:
class CodeGenerator(ABC):
def __init__(self, analyzer: SchemaAnalyzer):
self.analyzer = analyzer
self.schema = analyzer.schema
self.imports = analyzer.extensions.imports
@abstractmethod
def generate(self, *args, **kwargs) -> str:
pass
Key features:
Import management
Access to analyzed schema
AST generation helpers
AST Generation#
Code generators create Python AST nodes which are then formatted into code. Common patterns:
Class Generation
ast.ClassDef(
name=type_info.py_type,
bases=[ast_for_name("BaseModel")],
keywords=[],
body=body,
decorator_list=decorators,
)
Field Generation
ast_for_annotation_assignment(
self.name,
annotation=ast_for_name(self.type),
default=default
)
Extending the System#
To add new code generation capabilities:
New Generator
Create a new subclass of CodeGenerator:
class MyGenerator(CodeGenerator): def generate(self) -> str: body: List[ast.stmt] = [] # Add AST nodes to body module = self.create_module(body) return format_code(module)
New Metadata
Add handling in SchemaExtension:
class SchemaExtension: def __init__(self, schema: GraphQLSchema): self._my_metadata = schema.extensions.get("my_metadata", {})
New Type Categories
Extend SchemaAnalyzer:
class SchemaAnalyzer: def _analyze(self): self.my_types: List[MyType] = [] # Process types...
Best Practices#
- Type Handling
Always use parse_graphql_type for type processing
Handle nullability consistently
Consider forward references
- Metadata Processing
Validate metadata early
Provide clear error messages
Handle missing metadata gracefully
- AST Generation
Use utility functions in utils.py
Keep AST construction clean and organized
Handle imports carefully
- Testing
Add tests in test_codegen.py
Test edge cases and error conditions
Verify generated code validity
Common Tasks#
Adding a New Field Metadata Option
# In SchemaAnalyzer
def get_field(self, field_name: str, ...):
metadata = extensions.get_field_metadata(...)
# Handle new metadata
new_option = metadata.get("new_option")
# In Generator
def create_field_definition(self, field: Field):
if new_option := field.metadata.get("new_option"):
# Generate appropriate AST
Adding a New Type Category
# Create type class
@dataclasses.dataclass
class NewType:
name: str
# ...
# Add to SchemaAnalyzer
def _analyze(self):
self.new_types: List[NewType] = []
for name, type_def in self.schema.type_map.items():
if is_new_type(type_def):
self.new_types.append(self.parse_new_type(type_def))
Modifying Code Generation
class MyGenerator(CodeGenerator):
def render_object_type(self, type_info: ObjectType):
# Custom AST generation
return [
ast.ClassDef(
name=type_info.py_type,
# ...
)
]
Error Handling#
The system uses custom exceptions for schema validation:
class SchemaValidationError(Exception):
"""Raised when schema validation fails"""
pass
Key validation points:
Field nullability conflicts
Invalid relationships
Missing required metadata
Type reference issues
Development Workflow#
Make changes to code generation
Run tests: make test
Generate sample code to verify changes
Update documentation if needed
Add new tests for changes
Contributing#
When contributing changes:
Follow the existing code style
Add appropriate tests
Update documentation
Handle edge cases
Consider backward compatibility
Further Reading#
GraphQL AST documentation
Python AST module documentation
SQLAlchemy relationship documentation
Pydantic model documentation