Coverage for class_generator/parsers/type_parser.py: 98%
64 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"""Parser for type generation and property parsing from OpenAPI schemas."""
3import json
4import textwrap
5from pathlib import Path
6from typing import Any
8from simple_logger.logger import get_logger
10from class_generator.constants import MISSING_DESCRIPTION_STR, SCHEMA_DIR, SPEC_STR
11from class_generator.utils import sanitize_python_name
12from ocp_resources.utils.utils import convert_camel_case_to_snake_case
14LOGGER = get_logger(name=__name__)
17def types_generator(key_dict: dict[str, Any]) -> dict[str, str]:
18 """
19 Generate type information for a property.
21 Args:
22 key_dict: Property schema dictionary
24 Returns:
25 Dictionary with type information for init and documentation
26 """
27 type_for_docstring: str = "Any"
28 type_from_dict_for_init: str = ""
29 # A resource field may be defined with `x-kubernetes-preserve-unknown-fields`. In this case, `type` is not provided.
30 resource_type = key_dict.get("type")
32 # All fields must be set with Optional since resource can have yaml_file to cover all args.
33 if resource_type == "array":
34 type_for_docstring = "list[Any]"
36 elif resource_type == "string":
37 type_for_docstring = "str"
38 type_from_dict_for_init = f"{type_for_docstring} | None = None"
40 elif resource_type == "boolean":
41 type_for_docstring = "bool"
43 elif resource_type == "integer":
44 type_for_docstring = "int"
46 elif resource_type == "object":
47 type_for_docstring = "dict[str, Any]"
49 if not type_from_dict_for_init:
50 type_from_dict_for_init = f"{type_for_docstring} | None = None"
52 return {"type-for-init": type_from_dict_for_init, "type-for-doc": type_for_docstring}
55def get_property_schema(property_: dict[str, Any]) -> dict[str, Any]:
56 """
57 Resolve property schema, following $ref if needed.
59 Args:
60 property_: Property dictionary that may contain $ref
62 Returns:
63 Resolved property schema
64 """
65 # Handle direct $ref
66 if _ref := property_.get("$ref"):
67 # Extract the definition name from the $ref
68 # e.g., "#/definitions/io.k8s.api.core.v1.PodSpec" -> "io.k8s.api.core.v1.PodSpec"
69 # or "#/components/schemas/io.k8s.api.core.v1.PodSpec" -> "io.k8s.api.core.v1.PodSpec"
70 ref_name = _ref.split("/")[-1]
72 # Load from _definitions.json instead of individual files
73 definitions_file = Path(SCHEMA_DIR) / "_definitions.json"
74 if definitions_file.exists():
75 with open(definitions_file) as fd:
76 data = json.load(fd)
77 definitions = data.get("definitions", {})
78 if ref_name in definitions:
79 return definitions[ref_name]
81 # Fallback to the property itself if ref not found
82 LOGGER.warning(f"Could not resolve $ref: {_ref}")
84 # Handle allOf containing $ref
85 elif all_of := property_.get("allOf"):
86 # allOf is typically used with a single $ref in Kubernetes schemas
87 for item in all_of:
88 if "$ref" in item:
89 return get_property_schema(item)
91 return property_
94def format_description(description: str) -> str:
95 """Format description text for documentation."""
96 _res = ""
97 _text = textwrap.wrap(text=description, subsequent_indent=" ")
98 for _txt in _text:
99 _res += f"{_txt}\n"
101 return _res
104def prepare_property_dict(
105 schema: dict[str, Any],
106 required: list[str],
107 resource_dict: dict[str, Any],
108 dict_key: str,
109) -> dict[str, Any]:
110 """
111 Prepare property dictionary for template rendering.
113 Args:
114 schema: Schema properties
115 required: List of required property names
116 resource_dict: Resource dictionary to update
117 dict_key: Key to update in resource_dict ("spec" or "fields")
119 Returns:
120 Updated resource dictionary
121 """
122 keys_to_ignore: list[str] = ["kind", "apiVersion", "status", SPEC_STR.lower()]
123 keys_to_rename: set[str] = {"annotations", "labels"}
124 if dict_key != SPEC_STR.lower():
125 keys_to_ignore.append("metadata")
127 for key, val in schema.items():
128 if key in keys_to_ignore:
129 continue
131 val_schema = get_property_schema(property_=val)
132 type_dict = types_generator(key_dict=val_schema)
133 python_name = convert_camel_case_to_snake_case(name=f"{dict_key}_{key}" if key in keys_to_rename else key)
135 # Sanitize Python reserved keywords
136 safe_python_name, original_name = sanitize_python_name(name=python_name)
137 is_keyword_renamed = safe_python_name != original_name
139 resource_dict[dict_key].append({
140 "name-for-class-arg": safe_python_name,
141 "property-name": key,
142 "original-python-name": python_name, # Store original for reference
143 "is-keyword-renamed": is_keyword_renamed, # Flag for template
144 "required": key in required,
145 "description": format_description(description=val_schema.get("description", MISSING_DESCRIPTION_STR)),
146 "type-for-docstring": type_dict["type-for-doc"],
147 "type-for-class-arg": f"{safe_python_name}: {type_dict['type-for-init']}",
148 })
150 return resource_dict