from __future__ import annotations __all__ = ("generate",) from random import randint from typing import Tuple, Optional # , Any types = { "string": "str", "number": "float", "integer": "int", "boolean": "bool", "object": "dict", "array": "list", "null": "None", } # # Used for debugging # class AppendPrint(list): # def append(self, obj: Any) -> None: # if obj: # print(obj) # super().append(obj) def _generate( obj: dict, /, *, title: Optional[str] = None, no_comments: bool = False ) -> Tuple[str, str]: # sourcery skip: low-code-quality """Makes TypedDict from a JSON Schema object. Arguments: obj: JSON Schema dict. title: The title of the result. no_comments: If True, no comments will be added. Returns: The generated TypedDicts, the result values name and the comments. """ if title is None: title = f"GeneratedObject{randint(100, 200)}" # The other TypedDicts result = [] # The real annotation annotation: str obj_type = obj.get("type") # allOf, anyOf, oneOf, (not)? if not obj_type: # Treating oneOf as allOf, since is kinda the # same and there isn't a way to type it in Python if objs := obj.get("anyOf") or obj.get("oneOf"): union_items = [] optional = False for obj_schema in objs: if obj_schema["type"] == "null": optional = True else: extras, target = _generate(obj_schema, title=title, no_comments=no_comments) result.append(extras) union_items.append(target) annotation = f"{'Optional' if optional else 'Union'}[{', '.join(union_items)}]" else: annotation = "Any" # Union for parameters elif isinstance(obj_type, list): union_items = [] is_optional = False for obj_type_item in obj_type: if obj_type_item == "null": is_optional = True else: union_items.append(types[obj_type_item]) annotation = f"{'Optional' if is_optional else 'Optional'}[{', '.join(union_items)}]" elif obj_type == "boolean": annotation = "bool" elif obj_type == "null": annotation = "None" elif obj_type == "string": if not no_comments: if obj_min_len := obj.get("minLength"): result.append(f" # Minimum length: {obj_min_len}") if obj_max_len := obj.get("maxLength"): result.append(f" # Maximum length: {obj_max_len}") if obj_pattern := obj.get("pattern"): result.append(f" # Pattern: {obj_pattern!r}") annotation = "str" elif obj_type in {"integer", "number"}: if not no_comments: if obj_multiple := obj.get("multipleOf"): result.append(f" # Multiple of: {obj_multiple}") if obj_minimum := obj.get("minimum"): result.append(f" # Minimum (x >= N): {obj_minimum}") if obj_exclusive_minimum := obj.get("exclusiveMinimum"): result.append(f" # Exclusive minimum (x > N): {obj_exclusive_minimum}") if obj_maximum := obj.get("maximum"): result.append(f" # Maximum (x <= N): {obj_maximum}") if obj_exclusive_maximum := obj.get("exclusiveMaximum"): result.append(f" # Exclusive maximum (x < N): {obj_exclusive_maximum}") if any([obj_minimum, obj_exclusive_minimum, obj_maximum, obj_exclusive_maximum]): result.append(" # x = the variable, N = the min/max") annotation = "int" if obj_type == "integer" else "float" elif obj_type == "object": # TODO: add support for patternProperties, unevaluatedProperties, # propertyNames, minProperties, maxProperties if obj_properties := obj.get("properties"): # TODO: make it so the extra properties are typed instead of Any, somehow total = ", total=False" if obj.get("additionalProperties") else "" annotation = obj_title = obj.get("title", title).replace(" ", "") typed_dict = [f"class {obj_title}(TypedDict{total}):"] for key, value in obj_properties.items(): extras, param_annotation = _generate( value, title=key.capitalize(), no_comments=no_comments ) result.append(extras) if key not in obj.get("required", []): param_annotation = f"NotRequired[{param_annotation}]" if not no_comments: if examples := value.get("examples"): s = "" if len(examples) == 1 else "s" examples = ", ".join([str(example) for example in examples]).replace( "\n", "\\n" ) if (examples_short := examples[:70]) != examples: examples = f"{examples_short}[...]" examples = f" # Example{s}: {examples}" if examples else "" typed_dict.extend( [f" # Format: {fmt}" if (fmt := value.get("format")) else "", examples] ) typed_dict.append(f" {key}: {param_annotation}") result.extend(typed_dict) else: annotation = "dict" elif obj_type == "array": # TODO: add support for contains, minContains, # maxContains, minLength, maxLength, uniqueItems if obj_items := obj.get("items"): extras, target = _generate(obj_items, no_comments=no_comments) result.append(extras) annotation = f"List[{target}]" elif obj_prefix_items := obj.get("prefixItems"): tuple_annotation = [] for item in obj_prefix_items: ( extras, target, ) = _generate(item, no_comments=no_comments) result.append(extras) tuple_annotation.append(target) if extra_item_type := obj.get("items"): if extra_item_type is not True: extras, extra_type = _generate(extra_item_type, no_comments=no_comments) result.append(extras) if not no_comments: result.append( f" # The extra items for the tuple are typed as: {extra_type}" ) tuple_annotation.append("...") annotation = f"Tuple[{', '.join(tuple_annotation)}]" else: annotation = "list" else: annotation = "Any" result = [i for i in result if i] return "\n".join(result), annotation text = """from __future__ import annotations from typing import TYPE_CHECKING, Any, List, Optional, TypedDict, Union if TYPE_CHECKING: from typing_extensions import NotRequired {0[0]} {1} = {0[1]} """ def generate( schema: dict, /, *, object_name: Optional[str] = None, no_comments: bool = False ) -> str: generated = _generate(schema, title=object_name, no_comments=no_comments) return text.format(generated, object_name or "GeneratedObject")