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

1"""Parser for type generation and property parsing from OpenAPI schemas.""" 

2 

3import json 

4import textwrap 

5from pathlib import Path 

6from typing import Any 

7 

8from simple_logger.logger import get_logger 

9 

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 

13 

14LOGGER = get_logger(name=__name__) 

15 

16 

17def types_generator(key_dict: dict[str, Any]) -> dict[str, str]: 

18 """ 

19 Generate type information for a property. 

20  

21 Args: 

22 key_dict: Property schema dictionary 

23  

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") 

31 

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]" 

35 

36 elif resource_type == "string": 

37 type_for_docstring = "str" 

38 type_from_dict_for_init = f"{type_for_docstring} | None = None" 

39 

40 elif resource_type == "boolean": 

41 type_for_docstring = "bool" 

42 

43 elif resource_type == "integer": 

44 type_for_docstring = "int" 

45 

46 elif resource_type == "object": 

47 type_for_docstring = "dict[str, Any]" 

48 

49 if not type_from_dict_for_init: 

50 type_from_dict_for_init = f"{type_for_docstring} | None = None" 

51 

52 return {"type-for-init": type_from_dict_for_init, "type-for-doc": type_for_docstring} 

53 

54 

55def get_property_schema(property_: dict[str, Any]) -> dict[str, Any]: 

56 """ 

57 Resolve property schema, following $ref if needed. 

58  

59 Args: 

60 property_: Property dictionary that may contain $ref 

61  

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] 

71 

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] 

80 

81 # Fallback to the property itself if ref not found 

82 LOGGER.warning(f"Could not resolve $ref: {_ref}") 

83 

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) 

90 

91 return property_ 

92 

93 

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" 

100 

101 return _res 

102 

103 

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. 

112  

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") 

118  

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") 

126 

127 for key, val in schema.items(): 

128 if key in keys_to_ignore: 

129 continue 

130 

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) 

134 

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 

138 

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 }) 

149 

150 return resource_dict