Skip to content

Commit 9ecef4c

Browse files
authored
chore: file generation cleanup (#1736)
This PR does too many things :( 1. Major refactoring of the dev/scripts and dev/code-generation folders. Primarily this was removing duplicate code and cleaning up some poorly written code snippets as well as making them more idempotent so then can be re-run over and over again but still maintain the same results. This is working on my machine, but I've been having problems in CI and comparing diffs so running generators in CI will have to wait. 2. Re-Implement using the generated api routes for testing This was a _huge_ refactor that touched damn near every test file but now we have auto-generated typed routes with inline hints and it's used for nearly every test excluding a few that use classes for better parameterization. This should greatly reduce errors when writing new tests. 3. Minor Perf improvements for the All Recipes endpoint A. Removed redundant loops B. Uses orjson to do the encoding directly and returns a byte response instead of relying on the default jsonable_encoder. 4. Fix some TS type errors that cropped up for seemingly no reason half way through the PR. See this issue phillipdupuis/pydantic-to-typescript#28 Basically, the generated TS type is not-correct since Pydantic will automatically fill in null fields. The resulting TS type is generated with a ? to indicate it can be null even though we _know_ that i can't be.
1 parent a8f0fb1 commit 9ecef4c

File tree

107 files changed

+2465
-1893
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

107 files changed

+2465
-1893
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,4 @@ dev/code-generation/generated/openapi.json
158158
dev/code-generation/generated/test_routes.py
159159
mealie/services/parser_services/crfpp/model.crfmodel
160160
lcov.info
161+
dev/code-generation/openapi.json

dev/code-generation/_static.py

Lines changed: 0 additions & 26 deletions
This file was deleted.
File renamed without changes.

dev/code-generation/gen_test_data_paths.py renamed to dev/code-generation/gen_py_pytest_data_paths.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
from dataclasses import dataclass
22
from pathlib import Path
33

4-
from _gen_utils import log, render_python_template
54
from slugify import slugify
5+
from utils import render_python_template
66

77
CWD = Path(__file__).parent
88

@@ -25,9 +25,7 @@ def from_path(cls, path: Path):
2525

2626
# Remove any file extension
2727
var = var.split(".")[0]
28-
2928
var = var.replace("'", "")
30-
3129
var = slugify(var, separator="_")
3230

3331
return cls(var, rel_path)
@@ -97,8 +95,6 @@ def recursive_rename(p: Path):
9795

9896

9997
def main():
100-
log.info("Starting Template Generation")
101-
10298
rename_non_compliant_paths()
10399

104100
GENERATED.mkdir(exist_ok=True)
@@ -115,8 +111,6 @@ def main():
115111
{"children": all_children},
116112
)
117113

118-
log.info("Finished Template Generation")
119-
120114

121115
if __name__ == "__main__":
122116
main()
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import json
2+
from pathlib import Path
3+
4+
from fastapi import FastAPI
5+
from jinja2 import Template
6+
from pydantic import BaseModel
7+
from utils import PROJECT_DIR, CodeTemplates, HTTPRequest, RouteObject
8+
9+
CWD = Path(__file__).parent
10+
11+
OUTFILE = PROJECT_DIR / "tests" / "utils" / "api_routes" / "__init__.py"
12+
13+
14+
class PathObject(BaseModel):
15+
route_object: RouteObject
16+
http_verbs: list[HTTPRequest]
17+
18+
class Config:
19+
arbitrary_types_allowed = True
20+
21+
22+
def get_path_objects(app: FastAPI):
23+
paths = []
24+
25+
for key, value in app.openapi().items():
26+
if key == "paths":
27+
for key, value in value.items():
28+
29+
paths.append(
30+
PathObject(
31+
route_object=RouteObject(key),
32+
http_verbs=[HTTPRequest(request_type=k, **v) for k, v in value.items()],
33+
)
34+
)
35+
36+
return paths
37+
38+
39+
def dump_open_api(app: FastAPI):
40+
"""Writes the Open API as JSON to a json file"""
41+
OPEN_API_FILE = CWD / "openapi.json"
42+
43+
with open(OPEN_API_FILE, "w") as f:
44+
f.write(json.dumps(app.openapi()))
45+
46+
47+
def read_template(file: Path):
48+
with open(file) as f:
49+
return f.read()
50+
51+
52+
def generate_python_templates(static_paths: list[PathObject], function_paths: list[PathObject]):
53+
54+
template = Template(read_template(CodeTemplates.pytest_routes))
55+
content = template.render(
56+
paths={
57+
"prefix": "/api",
58+
"static_paths": static_paths,
59+
"function_paths": function_paths,
60+
}
61+
)
62+
with open(OUTFILE, "w") as f:
63+
f.write(content)
64+
65+
return
66+
67+
68+
def main():
69+
from mealie.app import app
70+
71+
dump_open_api(app)
72+
paths = get_path_objects(app)
73+
74+
static_paths = [x.route_object for x in paths if not x.route_object.is_function]
75+
function_paths = [x.route_object for x in paths if x.route_object.is_function]
76+
77+
static_paths.sort(key=lambda x: x.router_slug)
78+
function_paths.sort(key=lambda x: x.router_slug)
79+
80+
generate_python_templates(static_paths, function_paths)
81+
82+
83+
if __name__ == "__main__":
84+
main()
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import pathlib
2+
import re
3+
from dataclasses import dataclass, field
4+
5+
from utils import PROJECT_DIR, log, render_python_template
6+
7+
template = """# This file is auto-generated by gen_schema_exports.py
8+
{% for file in data.module.files %}{{ file.import_str() }}
9+
{% endfor %}
10+
11+
__all__ = [
12+
{% for file in data.module.files %}
13+
{%- for class in file.classes -%}
14+
"{{ class }}",
15+
{%- endfor -%}
16+
{%- endfor %}
17+
]
18+
19+
"""
20+
21+
SCHEMA_PATH = PROJECT_DIR / "mealie" / "schema"
22+
23+
SKIP = {"static", "__pycache__"}
24+
25+
26+
class PyFile:
27+
import_path: str
28+
"""The import path of the file"""
29+
30+
classes: list[str]
31+
"""A list of classes in the file"""
32+
33+
def __init__(self, path: pathlib.Path):
34+
self.import_path = path.stem
35+
self.classes = []
36+
37+
self.classes = PyFile.extract_classes(path)
38+
self.classes.sort()
39+
40+
def import_str(self) -> str:
41+
"""Returns a string that can be used to import the file"""
42+
return f"from .{self.import_path} import {', '.join(self.classes)}"
43+
44+
@staticmethod
45+
def extract_classes(file_path: pathlib.Path) -> list[str]:
46+
name = file_path.stem
47+
48+
if name == "__init__" or name.startswith("_"):
49+
return []
50+
51+
classes = re.findall(r"(?m)^class\s(\w+)", file_path.read_text())
52+
return classes
53+
54+
55+
@dataclass
56+
class Modules:
57+
directory: pathlib.Path
58+
"""The directory to search for modules"""
59+
60+
files: list[PyFile] = field(default_factory=list)
61+
"""A list of files in the directory"""
62+
63+
def __post_init__(self):
64+
for file in self.directory.glob("*.py"):
65+
if file.name.startswith("_"):
66+
continue
67+
68+
pfile = PyFile(file)
69+
70+
if len(pfile.classes) > 0:
71+
self.files.append(pfile)
72+
73+
else:
74+
log.debug(f"Skipping {file.name} as it has no classes")
75+
76+
77+
def find_modules(root: pathlib.Path) -> list[Modules]:
78+
"""Finds all the top level modules in the provided folder"""
79+
modules: list[Modules] = []
80+
for file in root.iterdir():
81+
if file.is_dir() and file.name not in SKIP:
82+
83+
modules.append(Modules(directory=file))
84+
85+
return modules
86+
87+
88+
def main():
89+
90+
modules = find_modules(SCHEMA_PATH)
91+
92+
for module in modules:
93+
log.debug(f"Module: {module.directory.name}")
94+
for file in module.files:
95+
log.debug(f" File: {file.import_path}")
96+
log.debug(f" Classes: [{', '.join(file.classes)}]")
97+
98+
render_python_template(template, module.directory / "__init__.py", {"module": module})
99+
100+
101+
if __name__ == "__main__":
102+
main()

dev/code-generation/gen_pytest_routes.py

Lines changed: 0 additions & 53 deletions
This file was deleted.

dev/code-generation/gen_schema_exports.py

Lines changed: 0 additions & 34 deletions
This file was deleted.

0 commit comments

Comments
 (0)