1
Fork 0
mirror of https://github.com/RGBCube/GitHubWrapper synced 2025-05-17 22:45:08 +00:00
GitHubWrapper/tools/unschema/parser.py
2022-06-28 16:27:00 +03:00

219 lines
7.3 KiB
Python

from __future__ import annotations
__all__ = ("generate",)
from random import randint
from typing import Optional, Tuple # , 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")