Coverage for class_generator/core/generator.py: 67%

91 statements  

« prev     ^ index     » next       coverage.py v7.10.1, created at 2025-07-29 12:31 +0300

1"""Core generation logic for resource classes.""" 

2 

3import filecmp 

4import os 

5import shlex 

6import sys 

7from pathlib import Path 

8from typing import Any 

9 

10from pyhelper_utils.shell import run_command 

11from rich.console import Console 

12from rich.syntax import Syntax 

13from simple_logger.logger import get_logger 

14 

15from class_generator.constants import RESOURCES_MAPPING_FILE, TESTS_MANIFESTS_DIR 

16from class_generator.core.schema import read_resources_mapping_file, update_kind_schema 

17from class_generator.formatters.file_writer import write_and_format_rendered 

18from class_generator.formatters.template_renderer import render_jinja_template 

19from class_generator.parsers.explain_parser import parse_explain 

20from class_generator.parsers.user_code_parser import parse_user_code_from_file 

21from ocp_resources.utils.utils import convert_camel_case_to_snake_case 

22 

23LOGGER = get_logger(name=__name__) 

24 

25 

26def generate_resource_file_from_dict( 

27 resource_dict: dict[str, Any], 

28 overwrite: bool = False, 

29 dry_run: bool = False, 

30 output_file: str = "", 

31 add_tests: bool = False, 

32 output_file_suffix: str = "", 

33 output_dir: str = "", 

34) -> tuple[str, str]: 

35 """ 

36 Generate a Python file from a resource dictionary. 

37  

38 Args: 

39 resource_dict: Dictionary containing resource information 

40 overwrite: Whether to overwrite existing files 

41 dry_run: If True, only print the output without writing 

42 output_file: Specific output file path 

43 add_tests: Whether to generate test files 

44 output_file_suffix: Suffix to add to the output filename 

45 output_dir: Output directory (defaults to "ocp_resources") 

46  

47 Returns: 

48 Tuple of (original_filename, generated_filename) 

49 """ 

50 base_dir = output_dir or "ocp_resources" 

51 if not os.path.exists(base_dir): 

52 os.makedirs(base_dir) 

53 

54 rendered = render_jinja_template( 

55 template_dict=resource_dict, 

56 template_dir="class_generator/manifests", 

57 template_name="class_generator_template.j2", 

58 ) 

59 

60 output = "# Generated using https://github.com/RedHatQE/openshift-python-wrapper/blob/main/scripts/resource/README.md\n\n" 

61 formatted_kind_str = convert_camel_case_to_snake_case(name=resource_dict["kind"]) 

62 _file_suffix: str = f"{'_' + output_file_suffix if output_file_suffix else ''}" 

63 

64 if add_tests: 

65 overwrite = True 

66 tests_path = os.path.join(TESTS_MANIFESTS_DIR, resource_dict["kind"]) 

67 if not os.path.exists(tests_path): 

68 os.makedirs(tests_path) 

69 

70 _output_file = os.path.join(tests_path, f"{formatted_kind_str}{_file_suffix}.py") 

71 

72 elif output_file: 

73 _output_file = output_file 

74 

75 else: 

76 _output_file = os.path.join(base_dir, f"{formatted_kind_str}{_file_suffix}.py") 

77 

78 _output_file_exists: bool = os.path.exists(_output_file) 

79 _user_code: str = "" 

80 _user_imports: str = "" 

81 

82 if _output_file_exists and not add_tests: 

83 _user_code, _user_imports = parse_user_code_from_file(file_path=_output_file) 

84 

85 orig_filename = _output_file 

86 if _output_file_exists: 

87 if overwrite: 

88 LOGGER.warning(f"Overwriting {_output_file}") 

89 

90 else: 

91 temp_output_file = _output_file.replace(".py", "_TEMP.py") 

92 LOGGER.warning(f"{_output_file} already exists, using {temp_output_file}") 

93 _output_file = temp_output_file 

94 

95 if _user_code.strip() or _user_imports.strip(): 

96 output += f"{_user_imports}{rendered}{_user_code}" 

97 else: 

98 output += rendered 

99 

100 if dry_run: 

101 _code = Syntax(code=output, lexer="python", line_numbers=True) 

102 Console().print(_code) 

103 

104 else: 

105 write_and_format_rendered(filepath=_output_file, output=output) 

106 

107 return orig_filename, _output_file 

108 

109 

110def class_generator( 

111 kind: str, 

112 overwrite: bool = False, 

113 dry_run: bool = False, 

114 output_file: str = "", 

115 output_dir: str = "", 

116 add_tests: bool = False, 

117 called_from_cli: bool = True, 

118 update_schema_executed: bool = False, 

119) -> list[str]: 

120 """ 

121 Generates a class for a given Kind. 

122  

123 Args: 

124 kind: Kubernetes resource kind 

125 overwrite: Whether to overwrite existing files 

126 dry_run: If True, only print the output without writing 

127 output_file: Specific output file path 

128 output_dir: Output directory 

129 add_tests: Whether to generate test files 

130 called_from_cli: Whether called from CLI (enables prompts) 

131 update_schema_executed: Whether schema update was already executed 

132  

133 Returns: 

134 List of generated file paths 

135 """ 

136 LOGGER.info(f"Generating class for {kind}") 

137 kind = kind.lower() 

138 kind_and_namespaced_mappings = read_resources_mapping_file().get(kind) 

139 if not kind_and_namespaced_mappings: 

140 if called_from_cli: 

141 if update_schema_executed: 

142 LOGGER.error(f"{kind} not found in {RESOURCES_MAPPING_FILE} after update-schema executed.") 

143 sys.exit(1) 

144 

145 run_update_schema = input( 

146 f"{kind} not found in {RESOURCES_MAPPING_FILE}, Do you want to run --update-schema and retry? [Y/N]" 

147 ) 

148 if run_update_schema.lower() == "n": 

149 sys.exit(1) 

150 

151 elif run_update_schema.lower() == "y": 

152 update_kind_schema() 

153 

154 return class_generator( 

155 overwrite=overwrite, 

156 dry_run=dry_run, 

157 kind=kind, 

158 output_file=output_file, 

159 output_dir=output_dir, 

160 add_tests=add_tests, 

161 called_from_cli=True, 

162 update_schema_executed=True, 

163 ) 

164 

165 else: 

166 LOGGER.error(f"{kind} not found in {RESOURCES_MAPPING_FILE}, Please run --update-schema.") 

167 return [] 

168 

169 resources = parse_explain(kind=kind) 

170 

171 # Check if we have resources from different API groups 

172 unique_groups = set() 

173 for resource in resources: 

174 # Use the original group name that we stored 

175 if "original_group" in resource and resource["original_group"]: 

176 unique_groups.add(resource["original_group"]) 

177 

178 use_output_file_suffix: bool = len(unique_groups) > 1 

179 generated_files: list[str] = [] 

180 for resource_dict in resources: 

181 # Use the lowercase version of the original group name for suffix 

182 output_file_suffix = "" 

183 if use_output_file_suffix and "original_group" in resource_dict and resource_dict["original_group"]: 

184 output_file_suffix = resource_dict["original_group"].lower().replace(".", "_").replace("-", "_") 

185 

186 orig_filename, generated_py_file = generate_resource_file_from_dict( 

187 resource_dict=resource_dict, 

188 overwrite=overwrite, 

189 dry_run=dry_run, 

190 output_file=output_file, 

191 add_tests=add_tests, 

192 output_file_suffix=output_file_suffix, 

193 output_dir=output_dir, 

194 ) 

195 

196 if not dry_run: 

197 run_command( 

198 command=shlex.split(f"uvx pre-commit run --files {generated_py_file}"), 

199 verify_stderr=False, 

200 check=False, 

201 ) 

202 

203 if orig_filename != generated_py_file and filecmp.cmp(orig_filename, generated_py_file): 

204 LOGGER.warning(f"File {orig_filename} was not updated, deleting {generated_py_file}") 

205 Path.unlink(Path(generated_py_file)) 

206 

207 generated_files.append(generated_py_file) 

208 

209 return generated_files