Coverage for formkit_ninja / parser / generator_config.py: 35.90%

56 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-03-06 04:12 +0000

1""" 

2Generator configuration for code generation. 

3 

4This module provides the GeneratorConfig class which holds all configuration 

5needed for code generation, including app name, output directory, NodePath class, 

6template packages, and custom imports. 

7""" 

8 

9import re 

10from pathlib import Path 

11from typing import Any, Optional, Type 

12 

13from pydantic import BaseModel, root_validator, validator 

14 

15from formkit_ninja.parser.type_convert import NodePath 

16 

17# Import DatabaseNodePath for default 

18try: 

19 from formkit_ninja.parser.database_node_path import DatabaseNodePath 

20 

21 DEFAULT_NODE_PATH_CLASS: type[NodePath] = DatabaseNodePath 

22except ImportError: 

23 # Fallback if database module not available 

24 DEFAULT_NODE_PATH_CLASS = NodePath 

25 

26 

27def schema_name_to_filename(schema_name: str) -> str: 

28 """ 

29 Convert a schema name to a valid Python module filename. 

30 

31 Examples: 

32 TF_6_1_1 -> tf611 

33 MySchema -> myschema 

34 Schema_1_2_3 -> schema123 

35 

36 Args: 

37 schema_name: The schema name/label 

38 

39 Returns: 

40 A valid Python module filename (lowercase, no special chars except underscores) 

41 """ 

42 # Remove all non-alphanumeric characters except underscores 

43 cleaned = re.sub(r"[^a-zA-Z0-9_]", "", schema_name) 

44 # Convert to lowercase 

45 cleaned = cleaned.lower() 

46 # Remove underscores 

47 cleaned = cleaned.replace("_", "") 

48 return cleaned 

49 

50 

51class GeneratorConfig(BaseModel): 

52 """ 

53 Configuration for code generation. 

54 

55 Attributes: 

56 app_name: Name of the Django app (required) 

57 output_dir: Directory where generated code will be written (required) 

58 node_path_class: Custom NodePath subclass to use (default: NodePath) 

59 template_packages: List of package paths for template loading (default: []) 

60 custom_imports: List of custom import statements to include (default: []) 

61 include_ordinality: Whether to include ordinality field in repeater models (default: True) 

62 merge_top_level_groups: Whether to merge top-level groups using abstract inheritance (default: False) 

63 schema_name: Optional schema name/label used for generating model filenames (default: None) 

64 """ 

65 

66 app_name: str 

67 output_dir: Path 

68 node_path_class: Type[NodePath] = DEFAULT_NODE_PATH_CLASS 

69 template_packages: list[str] = [] 

70 custom_imports: list[str] = [] 

71 include_ordinality: bool = True 

72 merge_top_level_groups: bool = False 

73 schema_name: Optional[str] = None 

74 

75 @validator("app_name") 

76 def validate_app_name(cls, v: str) -> str: 

77 """Validate that app_name is not empty.""" 

78 if not v or not v.strip(): 

79 raise ValueError("app_name cannot be empty") 

80 return v.strip() 

81 

82 @validator("output_dir", pre=True) 

83 def validate_output_dir(cls, v: str | Path) -> Path: 

84 """Convert string to Path if necessary.""" 

85 if isinstance(v, str): 

86 return Path(v) 

87 return v 

88 

89 @validator("node_path_class") 

90 def validate_node_path_class(cls, v: Type[NodePath]) -> Type[NodePath]: 

91 """Validate that node_path_class is a subclass of NodePath.""" 

92 if not isinstance(v, type) or not issubclass(v, NodePath): 

93 raise ValueError("node_path_class must be a subclass of NodePath") 

94 return v 

95 

96 @root_validator(pre=True) 

97 def validate_list_items_before_coercion(cls, values: dict[str, Any]) -> dict[str, Any]: 

98 """Validate that list items are strings before Pydantic coerces them.""" 

99 if "template_packages" in values: 

100 template_packages = values["template_packages"] 

101 if isinstance(template_packages, list): 

102 for item in template_packages: 

103 if not isinstance(item, str): 

104 raise ValueError("All items in template_packages must be strings") 

105 

106 if "custom_imports" in values: 

107 custom_imports = values["custom_imports"] 

108 if isinstance(custom_imports, list): 

109 for item in custom_imports: 

110 if not isinstance(item, str): 

111 raise ValueError("All items in custom_imports must be strings") 

112 

113 return values 

114 

115 class Config: 

116 """Pydantic configuration.""" 

117 

118 arbitrary_types_allowed = True