Coverage for formkit_ninja / parser / plugins.py: 38.98%

49 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-02-27 05:19 +0000

1""" 

2Plugin system for extending formkit-ninja code generation. 

3 

4This module provides: 

5- GeneratorPlugin: Abstract base class for plugins 

6- PluginRegistry: Registry for managing and discovering plugins 

7- register_plugin: Decorator for registering plugins 

8""" 

9 

10from __future__ import annotations 

11 

12from abc import ABC, abstractmethod 

13from typing import Callable, List, Type, Union 

14 

15from formkit_ninja.parser.converters import TypeConverterRegistry 

16from formkit_ninja.parser.type_convert import NodePath 

17 

18 

19class GeneratorPlugin(ABC): 

20 """ 

21 Abstract base class for code generator plugins. 

22 

23 Plugins can extend formkit-ninja functionality by: 

24 - Registering custom type converters 

25 - Providing additional template packages 

26 - Extending NodePath with custom functionality 

27 

28 Subclasses must implement all three methods. 

29 """ 

30 

31 @abstractmethod 

32 def register_converters(self, registry: TypeConverterRegistry) -> None: 

33 """ 

34 Register custom type converters with the registry. 

35 

36 This method is called during code generation setup to allow plugins 

37 to register their custom type converters. Converters are checked in 

38 priority order (higher priority first). 

39 

40 Args: 

41 registry: The TypeConverterRegistry to register converters with 

42 """ 

43 pass 

44 

45 @abstractmethod 

46 def get_template_packages(self) -> List[str]: 

47 """ 

48 Get list of template package names provided by this plugin. 

49 

50 Template packages should contain a "templates" subdirectory with 

51 Jinja2 templates. Packages are checked in order, with earlier 

52 packages taking precedence. 

53 

54 Returns: 

55 List of package names (e.g., ["myapp.templates", "formkit_ninja.parser"]) 

56 """ 

57 pass 

58 

59 @abstractmethod 

60 def extend_node_path(self) -> Type[NodePath] | None: 

61 """ 

62 Return a custom NodePath subclass or None. 

63 

64 If a plugin wants to extend NodePath functionality, it can return 

65 a subclass here. The first non-None return value from registered 

66 plugins will be used. 

67 

68 Returns: 

69 A NodePath subclass, or None if this plugin doesn't extend NodePath 

70 """ 

71 pass 

72 

73 

74class PluginRegistry: 

75 """ 

76 Registry for managing and discovering code generator plugins. 

77 

78 Plugins can be registered manually or discovered via entry points (future). 

79 The registry provides methods to: 

80 - Apply all plugin converters to a TypeConverterRegistry 

81 - Collect template packages from all plugins 

82 - Get the first non-None NodePath extension from plugins 

83 """ 

84 

85 def __init__(self) -> None: 

86 """Initialize an empty plugin registry.""" 

87 self._plugins: List[GeneratorPlugin] = [] 

88 

89 def register(self, plugin: GeneratorPlugin) -> None: 

90 """ 

91 Register a plugin instance. 

92 

93 Args: 

94 plugin: The plugin instance to register 

95 """ 

96 self._plugins.append(plugin) 

97 

98 def get_all_plugins(self) -> List[GeneratorPlugin]: 

99 """ 

100 Get all registered plugins. 

101 

102 Returns: 

103 List of all registered plugin instances 

104 """ 

105 return self._plugins.copy() 

106 

107 def apply_converters(self, registry: TypeConverterRegistry) -> None: 

108 """ 

109 Apply all plugin converters to the given registry. 

110 

111 This calls register_converters() on each registered plugin, allowing 

112 them to register their custom type converters. 

113 

114 Args: 

115 registry: The TypeConverterRegistry to register converters with 

116 """ 

117 for plugin in self._plugins: 

118 plugin.register_converters(registry) 

119 

120 def collect_template_packages(self) -> List[str]: 

121 """ 

122 Collect template packages from all registered plugins. 

123 

124 Returns: 

125 List of all template package names from all plugins 

126 """ 

127 packages: List[str] = [] 

128 for plugin in self._plugins: 

129 packages.extend(plugin.get_template_packages()) 

130 return packages 

131 

132 def get_node_path_class(self) -> Type[NodePath] | None: 

133 """ 

134 Get the first non-None NodePath extension from registered plugins. 

135 

136 Plugins are checked in registration order. The first plugin that 

137 returns a non-None NodePath subclass will be used. 

138 

139 Returns: 

140 The first non-None NodePath subclass, or None if no plugin extends NodePath 

141 """ 

142 for plugin in self._plugins: 

143 node_path_class = plugin.extend_node_path() 

144 if node_path_class is not None: 

145 return node_path_class 

146 return None 

147 

148 

149# Default global plugin registry 

150_default_registry = PluginRegistry() 

151 

152 

153def register_plugin( 

154 plugin_class: Type[GeneratorPlugin] | None = None, 

155 registry: PluginRegistry | None = None, 

156) -> Union[Type[GeneratorPlugin], Callable[[Type[GeneratorPlugin]], Type[GeneratorPlugin]]]: 

157 """ 

158 Decorator for registering a plugin class. 

159 

160 Usage: 

161 @register_plugin 

162 class MyPlugin(GeneratorPlugin): 

163 ... 

164 

165 @register_plugin(registry=my_registry) 

166 class MyPlugin(GeneratorPlugin): 

167 ... 

168 

169 Args: 

170 plugin_class: The plugin class to register (when used as decorator) 

171 registry: The PluginRegistry to register with. If None, uses the default registry. 

172 

173 Returns: 

174 The decorated class (unchanged) or a decorator function 

175 """ 

176 # If called with keyword arguments only (e.g., @register_plugin(registry=...)) 

177 if plugin_class is None: 

178 

179 def decorator(cls: Type[GeneratorPlugin]) -> Type[GeneratorPlugin]: 

180 target_registry = registry or _default_registry 

181 target_registry.register(cls()) 

182 return cls 

183 

184 return decorator 

185 

186 # If called directly as @register_plugin (no parentheses) 

187 target_registry = registry or _default_registry 

188 target_registry.register(plugin_class()) 

189 return plugin_class 

190 

191 

192def get_default_registry() -> PluginRegistry: 

193 """ 

194 Get the default global plugin registry. 

195 

196 Returns: 

197 The default PluginRegistry instance 

198 """ 

199 return _default_registry