Coverage for formkit_ninja / parser / database_node_path.py: 7.62%
137 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-03-06 04:12 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-03-06 04:12 +0000
1"""
2DatabaseNodePath: NodePath that reads configuration from database.
4This module provides DatabaseNodePath, a NodePath subclass that queries
5the CodeGenerationConfig model for type mappings and field arguments,
6enabling database-driven code generation configuration.
7"""
9from __future__ import annotations
11from typing import TYPE_CHECKING
13from django.conf import settings
15from formkit_ninja.parser.type_convert import NodePath
17if TYPE_CHECKING: 17 ↛ 18line 17 didn't jump to line 18 because the condition on line 17 was never true
18 from formkit_ninja.code_generation_config import CodeGenerationConfig
21class DatabaseNodePath(NodePath):
22 """
23 NodePath that reads type mappings from database.
25 Priority cascade:
26 1. CodeGenerationConfig matching node_name (if exists)
27 2. CodeGenerationConfig matching options_pattern
28 3. CodeGenerationConfig matching formkit_type
29 4. Django settings FORMKIT_NINJA['TYPE_MAPPINGS']
30 5. Default TypeConverterRegistry
32 Example usage:
33 config = GeneratorConfig(
34 app_name="myapp",
35 output_dir=Path("./generated"),
36 node_path_class=DatabaseNodePath,
37 )
38 """
40 def __init__(self, *nodes, **kwargs):
41 super().__init__(*nodes, **kwargs)
42 self._config_cache: dict[str, CodeGenerationConfig | None] = {}
44 def _get_config(self) -> "CodeGenerationConfig | None":
45 """
46 Get matching CodeGenerationConfig for current node.
48 Returns the highest priority config that matches:
49 1. node_name (exact match)
50 2. options_pattern (startswith match)
51 3. formkit_type (exact match, no node_name/options_pattern set)
53 Returns:
54 Matching CodeGenerationConfig or None
55 """
56 # Build cache key (include options to handle pattern matching)
57 formkit_val = getattr(self.node, "formkit", "")
58 name_val = getattr(self.node, "name", "")
59 options_val = str(getattr(self.node, "options", ""))
60 cache_key = f"{formkit_val}-{name_val}-{options_val}"
62 if cache_key in self._config_cache:
63 return self._config_cache[cache_key]
65 from formkit_ninja.code_generation_config import CodeGenerationConfig
67 # Priority 1: Try node_name match (highest priority)
68 if hasattr(self.node, "name") and self.node.name:
69 config = (
70 CodeGenerationConfig.objects.filter( # type: ignore[attr-defined]
71 is_active=True,
72 node_name=self.node.name,
73 )
74 .order_by("-priority")
75 .first()
76 )
77 if config:
78 self._config_cache[cache_key] = config
79 return config
81 # Priority 2: Try options pattern match
82 if hasattr(self.node, "options") and self.node.options:
83 options_str = str(self.node.options)
85 # Build filter - include formkit_type if available
86 filter_kwargs = {
87 "is_active": True,
88 "options_pattern__isnull": False,
89 }
90 if hasattr(self.node, "formkit"):
91 filter_kwargs["formkit_type"] = self.node.formkit
93 configs = CodeGenerationConfig.objects.filter(**filter_kwargs).order_by("-priority") # type: ignore[attr-defined]
95 for cfg in configs:
96 if cfg.options_pattern and options_str.startswith(cfg.options_pattern):
97 self._config_cache[cache_key] = cfg
98 return cfg
100 # Priority 3: Try formkit_type match (no node_name/options_pattern)
101 if hasattr(self.node, "formkit"):
102 config = (
103 CodeGenerationConfig.objects.filter( # type: ignore[attr-defined]
104 is_active=True,
105 formkit_type=self.node.formkit,
106 node_name__isnull=True,
107 options_pattern__isnull=True,
108 )
109 .order_by("-priority")
110 .first()
111 )
112 if config:
113 self._config_cache[cache_key] = config
114 return config
116 self._config_cache[cache_key] = None
117 return None
119 def _get_from_settings(self, field: str) -> str | dict | None:
120 """
121 Get configuration from Django settings.
123 Checks FORMKIT_NINJA settings in order:
124 1. NAME_MAPPINGS[node.name][field]
125 2. OPTIONS_MAPPINGS (pattern match)
126 3. TYPE_MAPPINGS[node.formkit][field]
128 Args:
129 field: Field name ('pydantic_type', 'django_type', 'django_args')
131 Returns:
132 Configuration value or None
133 """
134 formkit_settings = getattr(settings, "FORMKIT_NINJA", {})
136 # Check NAME_MAPPINGS first (highest priority in settings)
137 if hasattr(self.node, "name") and self.node.name:
138 name_mappings = formkit_settings.get("NAME_MAPPINGS", {})
139 if self.node.name in name_mappings:
140 mapping = name_mappings[self.node.name]
141 if field in mapping:
142 return mapping[field]
144 # Check OPTIONS_MAPPINGS
145 if hasattr(self.node, "options") and self.node.options:
146 options_str = str(self.node.options)
147 options_mappings = formkit_settings.get("OPTIONS_MAPPINGS", {})
148 for pattern, mapping in options_mappings.items():
149 if options_str.startswith(pattern):
150 if field in mapping:
151 return mapping[field]
153 # Check TYPE_MAPPINGS
154 if hasattr(self.node, "formkit"):
155 type_mappings = formkit_settings.get("TYPE_MAPPINGS", {})
156 if self.node.formkit in type_mappings:
157 mapping = type_mappings[self.node.formkit]
158 if field in mapping:
159 return mapping[field]
161 return None
163 def to_pydantic_type(self) -> str:
164 """
165 Get Pydantic type for this node.
166 Prioritizes fields already on the node (via super()).
167 """
168 # 1. Check if it's already on the node
169 res = super().to_pydantic_type()
170 # super().to_pydantic_type() returns converter.pydantic_type if not found on node.
171 # We need to know if it was found on the node or not.
172 if hasattr(self.node, "pydantic_field_type") and self.node.pydantic_field_type:
173 return res
175 # 2. Check database config
176 config = self._get_config()
177 if config and config.pydantic_type:
178 return config.pydantic_type
180 # Check Django settings
181 settings_type = self._get_from_settings("pydantic_type")
182 if settings_type:
183 return str(settings_type)
185 # Fall back to parent implementation (default converters)
186 return super().to_pydantic_type()
188 def to_django_type(self) -> str:
189 """
190 Get Django field type for this node.
191 Prioritizes fields already on the node (via super()).
192 """
193 # 1. Check if it's already on the node
194 if hasattr(self.node, "django_field_type") and self.node.django_field_type:
195 return self.node.django_field_type
197 # 2. Check database config
198 config = self._get_config()
199 if config and config.django_type:
200 return config.django_type
202 # Check Django settings
203 settings_type = self._get_from_settings("django_type")
204 if settings_type:
205 return str(settings_type)
207 # Fall back to parent implementation
208 return super().to_django_type()
210 def to_django_args(self) -> str:
211 """
212 Get Django field arguments for this node.
213 Prioritizes fields already on the node (via super()).
214 """
215 # 1. Check if it's already on the node
216 args = getattr(self.node, "django_field_args", {})
217 pos_args = getattr(self.node, "django_field_positional_args", [])
218 if args or pos_args:
219 return super().to_django_args()
221 # 2. Check database config
222 config = self._get_config()
223 if config:
224 return config.get_django_args_str()
226 # Check Django settings
227 settings_args = self._get_from_settings("django_args")
228 settings_pos_args = self._get_from_settings("django_positional_args")
230 if settings_args or settings_pos_args:
231 return self._django_args_dict_to_str(
232 settings_args if isinstance(settings_args, dict) else {},
233 settings_pos_args if isinstance(settings_pos_args, list) else [],
234 )
236 # Fall back to parent implementation
237 return super().to_django_args()
239 def get_validators(self) -> list[str]:
240 """
241 Get validators for this node.
242 Prioritizes fields already on the node (via super()).
243 """
244 # 1. Check if it's already on the node (handled by super())
245 res = super().get_validators()
246 # super().get_validators() returns an empty list if not found on node.
247 # If `res` is not empty, it means it came from the node.
248 if res:
249 return res
251 # 2. Check database config
252 config = self._get_config()
253 if config and config.validators:
254 return config.validators
256 # Check settings
257 settings_validators = self._get_from_settings("validators")
258 if settings_validators and isinstance(settings_validators, list):
259 return settings_validators
261 return []
263 def get_extra_imports(self) -> list[str]:
264 """
265 Get extra imports for this node.
266 Prioritizes fields already on the node (via super()).
267 """
268 # 1. Check if it's already on the node (handled by super())
269 res = super().get_extra_imports()
270 # super().get_extra_imports() returns an empty list if not found on node.
271 # If `res` is not empty, it means it came from the node.
272 if res:
273 return res
275 # 2. Check database config
276 config = self._get_config()
277 if config and config.extra_imports:
278 return config.extra_imports
280 # Check settings
281 settings_imports = self._get_from_settings("extra_imports")
282 if settings_imports and isinstance(settings_imports, list):
283 return settings_imports
285 return super().get_extra_imports()
287 @staticmethod
288 def _django_args_dict_to_str(args_dict: dict, positional_args: list | None = None) -> str:
289 """
290 Convert django_args dict and positional_args list to string format.
292 Args:
293 args_dict: Dict of keyword field arguments
294 positional_args: List of positional field arguments
296 Returns:
297 Comma-separated string of arguments
298 """
299 parts = []
301 # Handle positional arguments first
302 if positional_args:
303 for value in positional_args:
304 parts.append(str(value))
306 # Handle keyword arguments
307 for key, value in args_dict.items():
308 if isinstance(value, bool):
309 parts.append(f"{key}={str(value)}")
310 elif isinstance(value, (int, float)):
311 parts.append(f"{key}={value}")
312 elif isinstance(value, str):
313 # Handle model references (e.g., "app.Model" or models.CASCADE)
314 if value in {"True", "False", "None"}:
315 parts.append(f"{key}={value}")
316 elif value.startswith("models.") or ("." in value and not value.startswith('"')):
317 parts.append(f"{key}={value}")
318 else:
319 parts.append(f'{key}="{value}"')
320 else:
321 parts.append(f"{key}={value}")
323 return ", ".join(parts)