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
« prev ^ index » next coverage.py v7.10.1, created at 2025-07-29 12:31 +0300
1"""Core generation logic for resource classes."""
3import filecmp
4import os
5import shlex
6import sys
7from pathlib import Path
8from typing import Any
10from pyhelper_utils.shell import run_command
11from rich.console import Console
12from rich.syntax import Syntax
13from simple_logger.logger import get_logger
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
23LOGGER = get_logger(name=__name__)
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.
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")
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)
54 rendered = render_jinja_template(
55 template_dict=resource_dict,
56 template_dir="class_generator/manifests",
57 template_name="class_generator_template.j2",
58 )
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 ''}"
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)
70 _output_file = os.path.join(tests_path, f"{formatted_kind_str}{_file_suffix}.py")
72 elif output_file:
73 _output_file = output_file
75 else:
76 _output_file = os.path.join(base_dir, f"{formatted_kind_str}{_file_suffix}.py")
78 _output_file_exists: bool = os.path.exists(_output_file)
79 _user_code: str = ""
80 _user_imports: str = ""
82 if _output_file_exists and not add_tests:
83 _user_code, _user_imports = parse_user_code_from_file(file_path=_output_file)
85 orig_filename = _output_file
86 if _output_file_exists:
87 if overwrite:
88 LOGGER.warning(f"Overwriting {_output_file}")
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
95 if _user_code.strip() or _user_imports.strip():
96 output += f"{_user_imports}{rendered}{_user_code}"
97 else:
98 output += rendered
100 if dry_run:
101 _code = Syntax(code=output, lexer="python", line_numbers=True)
102 Console().print(_code)
104 else:
105 write_and_format_rendered(filepath=_output_file, output=output)
107 return orig_filename, _output_file
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.
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
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)
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)
151 elif run_update_schema.lower() == "y":
152 update_kind_schema()
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 )
165 else:
166 LOGGER.error(f"{kind} not found in {RESOURCES_MAPPING_FILE}, Please run --update-schema.")
167 return []
169 resources = parse_explain(kind=kind)
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"])
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("-", "_")
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 )
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 )
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))
207 generated_files.append(generated_py_file)
209 return generated_files