"""Template engine for code scaffolding.
This module provides a template engine for generating code files
and project scaffolding using Python's string.Template.
"""
import re
from pathlib import Path
from string import Template
from typing import Dict, Optional, Union
[docs]
class TemplateEngine:
"""Template engine for code generation and scaffolding.
This class provides methods to load and render templates with
variable substitution. Templates are stored as files and can
be rendered with custom variables.
Attributes:
template_dir: Directory containing template files
Example:
>>> engine = TemplateEngine()
>>> result = engine.render('service_page.py', {
... 'service_name': 'users-api',
... 'class_name': 'UsersApi'
... })
"""
[docs]
def __init__(self, template_dir: Optional[Union[str, Path]] = None):
"""Initialize the template engine.
Args:
template_dir: Directory containing template files.
If None, uses the default templates directory.
"""
if template_dir is None:
# Use the templates directory in the package
self.template_dir = Path(__file__).parent.parent / "templates"
else:
self.template_dir = Path(template_dir)
[docs]
def load_template(self, template_name: str) -> Template:
"""Load a template from file.
Args:
template_name: Name of the template file (with or without .template extension)
Returns:
Template: A string.Template instance
Raises:
FileNotFoundError: If template file doesn't exist
"""
# Ensure .template extension
if not template_name.endswith(".template"):
template_name = f"{template_name}.template"
template_path = self.template_dir / template_name
if not template_path.exists():
raise FileNotFoundError(
f"Template not found: {template_path}\n"
f"Available templates: {self.list_templates()}"
)
template_content = template_path.read_text(encoding="utf-8")
return Template(template_content)
[docs]
def render(self, template_name: str, variables: Optional[Dict[str, str]] = None) -> str:
"""Render a template with variable substitution.
Args:
template_name: Name of the template file
variables: Dictionary of variables for substitution
Returns:
str: Rendered template content
Raises:
FileNotFoundError: If template file doesn't exist
"""
if variables is None:
variables = {}
template = self.load_template(template_name)
return template.safe_substitute(variables)
[docs]
def render_to_file(
self,
template_name: str,
variables: Optional[Dict[str, str]],
output_path: Union[str, Path],
overwrite: bool = False,
) -> Path:
"""Render a template and write to file.
Args:
template_name: Name of the template file
variables: Dictionary of variables for substitution
output_path: Path where to write the rendered content
overwrite: If True, overwrite existing file
Returns:
Path: Path to the written file
Raises:
FileNotFoundError: If template file doesn't exist
FileExistsError: If output file exists and overwrite=False
"""
output_path = Path(output_path)
if output_path.exists() and not overwrite:
raise FileExistsError(
f"File already exists: {output_path}. " f"Use overwrite=True to overwrite."
)
# Ensure parent directory exists
output_path.parent.mkdir(parents=True, exist_ok=True)
content = self.render(template_name, variables)
output_path.write_text(content, encoding="utf-8")
return output_path
[docs]
def list_templates(self) -> list:
"""List all available templates.
Returns:
list: List of template file names
"""
if not self.template_dir.exists():
return []
return [
f.name for f in self.template_dir.iterdir() if f.is_file() and f.suffix == ".template"
]
[docs]
def get_template_variables(self, template_name: str) -> list:
"""Extract variable names from a template.
Args:
template_name: Name of the template file
Returns:
list: List of variable names used in the template
"""
template = self.load_template(template_name)
# Pattern to match ${var} or $var
pattern = r"\$\{([^}]+)\}|\$([a-zA-Z_][a-zA-Z0-9_]*)"
matches = re.findall(pattern, template.template)
# Extract variable names from matches
variables = []
for match in matches:
# match is a tuple (braced, unbraced)
var = match[0] if match[0] else match[1]
if var and var not in variables:
variables.append(var)
return variables
[docs]
def validate_variables(self, template_name: str, variables: Dict[str, str]) -> tuple:
"""Validate that all required variables are provided.
Args:
template_name: Name of the template file
variables: Dictionary of variables to validate
Returns:
tuple: (missing_vars, extra_vars)
- missing_vars: List of variables in template but not in variables dict
- extra_vars: List of variables in variables dict but not in template
"""
template_vars = set(self.get_template_variables(template_name))
provided_vars = set(variables.keys())
missing = list(template_vars - provided_vars)
extra = list(provided_vars - template_vars)
return missing, extra
[docs]
def to_class_name(name: str) -> str:
"""Convert a service name to a class name.
Converts names like 'users-api' or 'users_api' to 'UsersApi'.
Args:
name: Service name (e.g., 'users-api', 'users_api')
Returns:
str: Class name (e.g., 'UsersApi')
Example:
>>> to_class_name('users-api')
'UsersApi'
>>> to_class_name('my_service')
'MyService'
"""
# Replace hyphens and underscores with spaces, title case, then remove spaces
return name.replace("-", " ").replace("_", " ").title().replace(" ", "")
[docs]
def to_snake_case(name: str) -> str:
"""Convert a service name to snake_case.
Converts names like 'users-api' to 'users_api'.
Args:
name: Service name (e.g., 'users-api')
Returns:
str: Snake case name (e.g., 'users_api')
Example:
>>> to_snake_case('users-api')
'users_api'
>>> to_snake_case('MyService')
'my_service'
"""
# Replace hyphens with underscores and convert to lowercase
return name.replace("-", "_").lower()
[docs]
def to_camel_case(name: str) -> str:
"""Convert a service name to camelCase.
Converts names like 'users-api' to 'usersApi'.
Args:
name: Service name (e.g., 'users-api')
Returns:
str: Camel case name (e.g., 'usersApi')
Example:
>>> to_camel_case('users-api')
'usersApi'
>>> to_camel_case('my_service')
'myService'
"""
class_name = to_class_name(name)
return class_name[0].lower() + class_name[1:]