Coverage for formkit_ninja / parser / plugins.py: 38.98%
49 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-02-20 04:40 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-02-20 04:40 +0000
1"""
2Plugin system for extending formkit-ninja code generation.
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"""
10from __future__ import annotations
12from abc import ABC, abstractmethod
13from typing import Callable, List, Type, Union
15from formkit_ninja.parser.converters import TypeConverterRegistry
16from formkit_ninja.parser.type_convert import NodePath
19class GeneratorPlugin(ABC):
20 """
21 Abstract base class for code generator plugins.
23 Plugins can extend formkit-ninja functionality by:
24 - Registering custom type converters
25 - Providing additional template packages
26 - Extending NodePath with custom functionality
28 Subclasses must implement all three methods.
29 """
31 @abstractmethod
32 def register_converters(self, registry: TypeConverterRegistry) -> None:
33 """
34 Register custom type converters with the registry.
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).
40 Args:
41 registry: The TypeConverterRegistry to register converters with
42 """
43 pass
45 @abstractmethod
46 def get_template_packages(self) -> List[str]:
47 """
48 Get list of template package names provided by this plugin.
50 Template packages should contain a "templates" subdirectory with
51 Jinja2 templates. Packages are checked in order, with earlier
52 packages taking precedence.
54 Returns:
55 List of package names (e.g., ["myapp.templates", "formkit_ninja.parser"])
56 """
57 pass
59 @abstractmethod
60 def extend_node_path(self) -> Type[NodePath] | None:
61 """
62 Return a custom NodePath subclass or None.
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.
68 Returns:
69 A NodePath subclass, or None if this plugin doesn't extend NodePath
70 """
71 pass
74class PluginRegistry:
75 """
76 Registry for managing and discovering code generator plugins.
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 """
85 def __init__(self) -> None:
86 """Initialize an empty plugin registry."""
87 self._plugins: List[GeneratorPlugin] = []
89 def register(self, plugin: GeneratorPlugin) -> None:
90 """
91 Register a plugin instance.
93 Args:
94 plugin: The plugin instance to register
95 """
96 self._plugins.append(plugin)
98 def get_all_plugins(self) -> List[GeneratorPlugin]:
99 """
100 Get all registered plugins.
102 Returns:
103 List of all registered plugin instances
104 """
105 return self._plugins.copy()
107 def apply_converters(self, registry: TypeConverterRegistry) -> None:
108 """
109 Apply all plugin converters to the given registry.
111 This calls register_converters() on each registered plugin, allowing
112 them to register their custom type converters.
114 Args:
115 registry: The TypeConverterRegistry to register converters with
116 """
117 for plugin in self._plugins:
118 plugin.register_converters(registry)
120 def collect_template_packages(self) -> List[str]:
121 """
122 Collect template packages from all registered plugins.
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
132 def get_node_path_class(self) -> Type[NodePath] | None:
133 """
134 Get the first non-None NodePath extension from registered plugins.
136 Plugins are checked in registration order. The first plugin that
137 returns a non-None NodePath subclass will be used.
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
149# Default global plugin registry
150_default_registry = PluginRegistry()
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.
160 Usage:
161 @register_plugin
162 class MyPlugin(GeneratorPlugin):
163 ...
165 @register_plugin(registry=my_registry)
166 class MyPlugin(GeneratorPlugin):
167 ...
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.
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:
179 def decorator(cls: Type[GeneratorPlugin]) -> Type[GeneratorPlugin]:
180 target_registry = registry or _default_registry
181 target_registry.register(cls())
182 return cls
184 return decorator
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
192def get_default_registry() -> PluginRegistry:
193 """
194 Get the default global plugin registry.
196 Returns:
197 The default PluginRegistry instance
198 """
199 return _default_registry