mirror of
https://github.com/RGBCube/GitHubWrapper
synced 2025-05-18 15:05:08 +00:00
rewrite parser
This commit is contained in:
parent
7e25087df4
commit
f2988d3405
3 changed files with 201 additions and 150 deletions
|
@ -1 +1 @@
|
|||
from .parser import generate
|
||||
from .parser import generate_typed_dicts_from_json_schema
|
||||
|
|
|
@ -1,50 +1,67 @@
|
|||
import json
|
||||
import os
|
||||
import time
|
||||
from argparse import ArgumentParser
|
||||
|
||||
from .parser import generate
|
||||
from .parser import generate_typed_dicts_from_json_schema
|
||||
|
||||
parser = ArgumentParser(description="Generate TypedDicts from a json schema.")
|
||||
parser.add_argument("-f", "--from", required=True, help="The json schema to generate from.")
|
||||
parser.add_argument("-t", "--to", help="The file to write the TypedDicts to.")
|
||||
parser.add_argument(
|
||||
"-ne", "--no-examples", action="store_true", help="If given, examples wont be added."
|
||||
"-on",
|
||||
"--object-name",
|
||||
default="GeneratedObjectResult",
|
||||
help="The name of the object to generate.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-nf", "--no-formats", action="store_true", help="If given, formats wont be added."
|
||||
"-nc", "--no-comments", action="store_true", help="If given, comments wont be added."
|
||||
)
|
||||
parser.add_argument(
|
||||
"-p",
|
||||
"--print",
|
||||
action="store_true",
|
||||
help="If given, the result will be printed instead of being writing to a file",
|
||||
help="If given, the result will be printed.",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
with open(args.__getattribute__("from")) as g:
|
||||
schema = json.load(g)
|
||||
with open(args.__getattribute__("from")) as f:
|
||||
generated = generate_typed_dicts_from_json_schema(json.load(f), no_comments=args.no_comments)
|
||||
|
||||
start = time.monotonic()
|
||||
text = f"""
|
||||
from __future__ import annotations
|
||||
start = time.perf_counter()
|
||||
|
||||
from typing import List, Optional, TypedDict, Union, TYPE_CHECKING
|
||||
text = f"""from __future__ import annotations
|
||||
|
||||
from typing import Any, List, Optional, TypedDict, Union, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import NotRequired{generate(schema, no_examples=args.no_examples, no_formats=args.no_formats)} # Rename this to your liking.
|
||||
"""[
|
||||
1:
|
||||
]
|
||||
from typing_extensions import NotRequired
|
||||
|
||||
{generated[0]}
|
||||
|
||||
{args.object_name} = {generated[1]}
|
||||
"""
|
||||
|
||||
if args.print:
|
||||
print(text)
|
||||
|
||||
end = time.perf_counter() - start
|
||||
|
||||
else:
|
||||
if not args.to:
|
||||
if not (to := args.to):
|
||||
raise ValueError("-t/--to is required when writing to a file.")
|
||||
|
||||
with open(args.to, "w") as f:
|
||||
with open(to, "w") as f:
|
||||
f.write(text)
|
||||
|
||||
end = time.monotonic() - start
|
||||
end = time.perf_counter() - start
|
||||
|
||||
exit_code = os.system(f"unimport {to} --gitignore -r --ignore-init; isort {to}; black {to}; flynt {to} -tc", )
|
||||
|
||||
if exit_code != 0:
|
||||
print(f"Formatting failed with exit code {exit_code}")
|
||||
|
||||
print(f"\n\nSuccess! Finished in {end*1000:.3} milliseconds")
|
||||
|
||||
|
||||
print(f"Success! Finished in {end*1000:.3} milliseconds.")
|
||||
|
|
|
@ -1,155 +1,189 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Iterable, Tuple
|
||||
from typing import Tuple # , Any
|
||||
|
||||
types = {
|
||||
"string": "str",
|
||||
"integer": "int",
|
||||
"number": "float",
|
||||
"integer": "int",
|
||||
"boolean": "bool",
|
||||
"object": "dict",
|
||||
"array": "list",
|
||||
"null": "None",
|
||||
}
|
||||
|
||||
untypes = {v: k for k, v in types.items()}
|
||||
|
||||
# # Used for debugging
|
||||
# class AppendPrint(list):
|
||||
# def append(self, obj: Any) -> None:
|
||||
# if obj:
|
||||
# print(obj)
|
||||
# super().append(obj)
|
||||
|
||||
|
||||
def unspace(s: str) -> str:
|
||||
return s.replace(" ", "")
|
||||
def generate_typed_dicts_from_json_schema(
|
||||
obj: dict, /, *, title: str = "GeneratedObject", 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.
|
||||
|
||||
def mklist(s: Iterable) -> str:
|
||||
return f"[{', '.join(s)}]"
|
||||
Returns:
|
||||
The generated TypedDicts, the result values name and the comments.
|
||||
"""
|
||||
# The other TypedDicts
|
||||
result = []
|
||||
|
||||
# The real annotation
|
||||
annotation: str
|
||||
|
||||
def generate(
|
||||
_schema: dict,
|
||||
/,
|
||||
*,
|
||||
no_examples: bool = False,
|
||||
no_formats: bool = False,
|
||||
title: str = "GeneratedObject",
|
||||
) -> str:
|
||||
def inner_generate(
|
||||
schema: dict,
|
||||
/,
|
||||
*,
|
||||
_no_examples: bool = no_examples,
|
||||
_no_formats: bool = no_formats,
|
||||
_title: str = title,
|
||||
) -> Tuple[str, str]: # sourcery skip: low-code-quality
|
||||
obj_type = obj.get("type")
|
||||
|
||||
# GeneratedObject = Union[..., ..., ...]
|
||||
if objects := schema.get("oneOf"):
|
||||
text = []
|
||||
# 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 = []
|
||||
|
||||
for item_schema in objects:
|
||||
generated_text, union_item = inner_generate(item_schema)
|
||||
|
||||
text.append(generated_text)
|
||||
union_items.append(union_item)
|
||||
|
||||
text.append(f"{_title} = Union{mklist(union_items)}")
|
||||
return "\n\n".join(_title), _title
|
||||
del objects
|
||||
|
||||
# class GeneratedObject(TypedDict):
|
||||
if (object_type := schema["type"]) == untypes["dict"]:
|
||||
_title = unspace(schema["title"])
|
||||
|
||||
dependencies = []
|
||||
current = [f'class {_title}(TypedDict):\n """{schema["description"]}"""']
|
||||
|
||||
for key, value in schema["properties"].items():
|
||||
com = ""
|
||||
if union_list := value.get("anyOf"):
|
||||
union_list.remove({"type": "null"})
|
||||
|
||||
if len(union_list) == 1:
|
||||
text, target = inner_generate(
|
||||
union_list[0],
|
||||
_title=_title,
|
||||
_no_examples=_no_examples,
|
||||
_no_formats=_no_formats,
|
||||
)
|
||||
dependencies.append(text)
|
||||
param_type = f"Optional[{target}]"
|
||||
|
||||
else:
|
||||
union_items = []
|
||||
|
||||
for item_schema in union_list:
|
||||
generated_text, union_item = generate(item_schema, title=_title)
|
||||
|
||||
dependencies.append(generated_text)
|
||||
union_items.append(union_item)
|
||||
|
||||
dependencies.append(f"{_title} = Union{mklist(union_items)}")
|
||||
|
||||
param_type = f"Optional[{_title}]"
|
||||
|
||||
elif isinstance(value_type := value["type"], str):
|
||||
param_type = types[value_type]
|
||||
|
||||
elif isinstance(value_type, list):
|
||||
combiner = "Union"
|
||||
contents = []
|
||||
|
||||
for type in value_type:
|
||||
if type == untypes["None"]:
|
||||
combiner = "Optional"
|
||||
else:
|
||||
contents.append(types[type])
|
||||
|
||||
param_type = f"{combiner}{mklist(contents)}"
|
||||
|
||||
elif isinstance(value_type, dict):
|
||||
text, param_type = inner_generate(value)
|
||||
dependencies.append(text)
|
||||
|
||||
optional = False
|
||||
for obj_schema in objs:
|
||||
if obj_schema["type"] == "null":
|
||||
optional = True
|
||||
else:
|
||||
param_type = f"Unknown[{value_type}]"
|
||||
com = "# "
|
||||
|
||||
if key not in schema["required"]:
|
||||
param_type = f"NotRequired[{param_type}]"
|
||||
|
||||
eg = (
|
||||
""
|
||||
if no_examples
|
||||
else f" # example: {mklist(str(eg) for eg in egs)[1:-1]}\n"
|
||||
if (egs := value.get("examples"))
|
||||
else ""
|
||||
)
|
||||
|
||||
fmt = (
|
||||
""
|
||||
if no_formats
|
||||
else f" # format: {fmt}\n"
|
||||
if (fmt := value.get("format"))
|
||||
else ""
|
||||
)
|
||||
|
||||
sep = "\n" if eg or fmt else ""
|
||||
|
||||
current.append(f"{sep}{eg}{fmt} {com}{key}: {param_type}")
|
||||
|
||||
dependencies.append("\n".join(current))
|
||||
result = "\n\n".join(dependencies[:-1]) + "\n\n\n" + dependencies[-1]
|
||||
|
||||
return result, _title
|
||||
|
||||
# GeneratedObject = List[...]
|
||||
elif isinstance(object_type, list):
|
||||
generated, target = inner_generate(
|
||||
schema["items"], _title=_title, _no_examples=_no_examples, _no_formats=_no_formats
|
||||
)
|
||||
return f"{generated}\n\n{_title} = List[{target}]", _title
|
||||
extras, target = generate_typed_dicts_from_json_schema(
|
||||
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:
|
||||
print("Unknown/Unimplemented Object Type:", object_type)
|
||||
return "", ""
|
||||
annotation = "Any"
|
||||
|
||||
return inner_generate(_schema)[0]
|
||||
# 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(" ", "")
|
||||
result.append(f"class {obj_title}(TypedDict{total}):")
|
||||
|
||||
for key, value in obj_properties.items():
|
||||
extras, param_annotation = generate_typed_dicts_from_json_schema(
|
||||
value, title=key.capitalize(), no_comments=no_comments
|
||||
)
|
||||
result.append(extras)
|
||||
if param_annotation not in obj.get("required", []):
|
||||
param_annotation = f"NotRequired[{param_annotation}]"
|
||||
|
||||
if not no_comments:
|
||||
result.extend(
|
||||
[
|
||||
f" # Format: {fmt}" if (fmt := value.get("format")) else "",
|
||||
f" # Example: {', '.join(str(ex) for ex in exs)}"
|
||||
if (exs := value.get("examples"))
|
||||
else "",
|
||||
]
|
||||
)
|
||||
|
||||
result.append(f" {key}: {param_annotation}")
|
||||
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_typed_dicts_from_json_schema(
|
||||
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_typed_dicts_from_json_schema(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_typed_dicts_from_json_schema(
|
||||
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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue