Skip to content

Commit 455c8fd

Browse files
committed
Avoid collisions between cases, enums, variants, and records of the same name by putting them in different modules.
1 parent 4593687 commit 455c8fd

2 files changed

Lines changed: 82 additions & 35 deletions

File tree

tools/generate_patches.py

Lines changed: 82 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,17 @@
1919
#
2020
# I want to avoid setattr and string repr of methods so IDEs have a chance. OTOH, IDEs don't give a crap about the exception wrapper; I'm not changing the signature as far as Python is concerned.
2121

22+
from collections import defaultdict
2223
import json
24+
from pathlib import Path
2325
from subprocess import check_output
2426
import textwrap
2527
from typing import Any, Iterable, Self
2628
from types import NoneType
2729

30+
2831
WIT_DIR = "wit"
32+
PACKAGE = "fastly:compute@0.0.0-prerelease.0"
2933

3034

3135
class DocsHaver:
@@ -66,7 +70,7 @@ class Type(DocsHaver):
6670
"""
6771

6872
@classmethod
69-
def from_id(cls, type_id: int | str | None, types_json: list[dict]) -> Self:
73+
def from_id(cls, type_id: int | str | None, wit_json: list[dict]) -> Self:
7074
"""Construct a type of the given index in the WIT's "types" key.
7175
7276
If that type is an alias (which is the case when referencing a type from
@@ -78,30 +82,30 @@ def from_id(cls, type_id: int | str | None, types_json: list[dict]) -> Self:
7882
while True:
7983
if isinstance(type_id, str):
8084
# It's a primitive.
81-
return cls(type_id, type_id, types_json)
85+
return cls(type_id, type_id, wit_json)
8286
elif isinstance(type_id, NoneType):
8387
return NullType()
8488

8589
# It's an int. Chase that down, including following type aliases.
86-
current_type = types_json[type_id]
90+
current_type = wit_json["types"][type_id]
8791
next_type = current_type["kind"].get("type")
8892
if next_type is None:
8993
kind = only(current_type["kind"].keys())
9094
class_ = KINDS_TO_CLASSES.get(kind, cls)
91-
return class_(type_id, current_type, types_json)
95+
return class_(type_id, current_type, wit_json)
9296
else:
9397
# It's a pointer to a different tyoe.
9498
type_id = next_type
9599

96-
def __init__(self, id: int | str, type_: dict | str, types_json: list[dict]):
100+
def __init__(self, id: int | str, type_: dict | str, wit_json: list[dict]):
97101
"""Private constructor. Use from_id() instead.
98102
99103
Type instances compare and hash based on their IDs: their positions in
100104
the WIT's type array. Only non-alias type IDs occur in instances.
101105
"""
102106
self._id: int | str = id
103107
self._type: str | dict[str] = type_
104-
self._types = types_json
108+
self._wit = wit_json
105109

106110
def name(self) -> str:
107111
"""Return the name of this type, in usual WIT kebab case."""
@@ -122,13 +126,17 @@ def __repr__(self):
122126
def has_cases(self):
123127
return False
124128

129+
def interface(self) -> "Interface":
130+
"""Return the interface where this type is defined."""
131+
return Interface(self._wit["interfaces"][self._type["owner"]["interface"]], self._wit)
132+
125133

126134
class Result(Type):
127135
"""A WIT ``result``"""
128136

129137
def error_type(self):
130138
"""Return the type of my error case."""
131-
return self.from_id(self._type["kind"]["result"]["err"], self._types)
139+
return self.from_id(self._type["kind"]["result"]["err"], self._wit)
132140

133141

134142
class NullType(Type):
@@ -174,9 +182,9 @@ def cases(self):
174182

175183

176184
class Function:
177-
def __init__(self, function_json: dict, types_json: list[dict]):
185+
def __init__(self, function_json: dict, wit_json: list[dict]):
178186
self._function = function_json
179-
self._types = types_json
187+
self._wit = wit_json
180188

181189
def error_type_of_returned_result(self) -> Type | None:
182190
"""If this Function returns a single ``result`` type, return the type of its error case.
@@ -188,30 +196,32 @@ def error_type_of_returned_result(self) -> Type | None:
188196
if num_returns > 1:
189197
raise NotImplementedError("Multi-value return isn't supported yet.")
190198
if num_returns == 1:
191-
return_type = Type.from_id(returns[0]["type"], self._types)
199+
return_type = Type.from_id(returns[0]["type"], self._wit)
192200
if isinstance(return_type, Result):
193201
return return_type.error_type()
194202

195203

196204
class Interface:
197-
def __init__(self, interface_json: dict[str, Any], types_json: list[dict]):
205+
def __init__(self, interface_json: dict[str, Any], wit_json: list[dict]):
198206
self._interface = interface_json
199-
self._types = types_json
207+
self._wit = wit_json
208+
209+
def name(self) -> str:
210+
return self._interface["name"]
200211

201212
def functions(self) -> Iterable[Function]:
202213
for function in self._interface["functions"].values():
203-
yield Function(function, self._types)
214+
yield Function(function, self._wit)
204215

205216

206217
class Package:
207-
def __init__(self, package_json: dict, interfaces_json: list[dict], types_json: list[dict]):
218+
def __init__(self, package_json: dict, wit_json: list[dict]):
208219
self._package = package_json
209-
self._interfaces = interfaces_json
210-
self._types = types_json
220+
self._wit = wit_json
211221

212222
def interfaces(self) -> Iterable[Interface]:
213223
for interface_num in self._package["interfaces"].values():
214-
yield Interface(self._interfaces[interface_num], self._types)
224+
yield Interface(self._wit["interfaces"][interface_num], self._wit)
215225

216226

217227
class Wit:
@@ -231,7 +241,7 @@ def __init__(self, wit_json: dict[str, list[dict]]):
231241
232242
:arg wit_json: The loaded JSON out of ``wasm-tools component wit wit/ --json``
233243
"""
234-
self._packages: dict[str, Package] = {p["name"]: Package(p, wit_json["interfaces"], wit_json["types"]) for p in wit_json["packages"]}
244+
self._packages: dict[str, Package] = {p["name"]: Package(p, wit_json) for p in wit_json["packages"]}
235245

236246
def package(self, name: str) -> Package:
237247
"""Return a package of the given name, e.g.
@@ -240,10 +250,15 @@ def package(self, name: str) -> Package:
240250

241251

242252
def upper_camel(s: str):
243-
"""Convert lower-kebab case to upper camel case."""
253+
"""Convert lower-kebab case to UpperCamelCase."""
244254
return "".join(word.capitalize() for word in s.split('-'))
245255

246256

257+
def lower_snake(s: str):
258+
"""Convert lower-kebab case to lower_snake_case."""
259+
return s.replace("-", "_")
260+
261+
247262
def indent(s: str):
248263
"""Indent as for a docstring.
249264
@@ -253,28 +268,60 @@ def indent(s: str):
253268
return textwrap.indent(s, " ").strip()
254269

255270

256-
def generate_exception_classes(error_types: Iterable[Type]):
271+
def exception_code_tree(error_types: Iterable[Type]) -> dict[str, dict[str, str]]:
257272
"""Generate Python exception classes we can map variant and enum cases to.
258273
259274
Inherit the names and docstrings from the WIT. Create a common superclass
260275
for each type so you can catch the whole smear if you like.
276+
277+
:return: A dict of package names pointing to module names pointing to
278+
contained code. For example, acl (from the interface name) -> acl_error
279+
(from the enum name) -> class AclError(FastlyError)...
261280
"""
262-
ret = ""
281+
# interface name -> module name -> code chunks:
282+
code = defaultdict(lambda: defaultdict(str))
283+
263284
for error_type in error_types:
264-
error_class_name = upper_camel(error_type.name())
265-
# Common superclass for exceptions based on the enum or variant's
266-
# members. Also functions as the raised exception itself for records.
267-
ret += (f'''class {error_class_name}(FastlyError):\n'''
268-
f''' """{error_type.docstring_or_pass()}"""\n\n\n''')
285+
package = lower_snake(error_type.interface().name())
269286
if error_type.has_cases(): # It's an enum or variant.
270-
for case in error_type.cases():
271-
ret += (f'''class {upper_camel(case.name())}({error_class_name}):\n'''
272-
f''' """{case.docstring_or_pass()}"""\n\n\n''')
273-
elif not isinstance(error_type, Record):
287+
module = lower_snake(error_type.name()) + ".py"
288+
elif isinstance(error_type, Record):
289+
module = "__init__.py"
290+
else:
274291
raise NotImplementedError(
275292
"Only variants, enums, and records are currently handled as "
276293
"``result`` error types. Looks like it's time to support others!")
277-
return ret
294+
295+
# Create package's empty __init__.py if not already there:
296+
code[package]["__init__.py"]
297+
298+
error_class_name = upper_camel(error_type.name())
299+
# Common superclass for exceptions based on the enum or variant's
300+
# members. Or the raised exception itself for records.
301+
code[package][module] += (
302+
f'''class {error_class_name}(FastlyError):\n'''
303+
f''' """{error_type.docstring_or_pass()}"""\n\n\n''')
304+
if error_type.has_cases():
305+
# Insert enum or variant cases.
306+
for case in error_type.cases():
307+
code[package][module] += (
308+
f'''class {upper_camel(case.name())}({error_class_name}):\n'''
309+
f''' """{case.docstring_or_pass()}"""\n\n\n''')
310+
return code
311+
312+
313+
def write_files(tree: dict[str, dict[str, str]], base_folder: Path):
314+
"""Create filesystem artifacts mirroring a nested dict representing folders,
315+
then files, then file contents.
316+
317+
Overwrite files that are mentioned in ``tree``, but don't delete anything else.
318+
"""
319+
for folder, files in tree.items():
320+
folder_path = base_folder / folder
321+
folder_path.mkdir(parents=True, exist_ok=True)
322+
for file, contents in files.items():
323+
file_path = folder_path / file
324+
file_path.write_text(contents)
278325

279326

280327
def main():
@@ -303,7 +350,7 @@ def main():
303350
exceptions_to_generate: dict[Type, bool] = {}
304351
functions_to_patch = []
305352

306-
for interface in wit.package("fastly:compute@0.0.0-prerelease.0").interfaces():
353+
for interface in wit.package(PACKAGE).interfaces():
307354
for function in interface.functions():
308355
if error_type := function.error_type_of_returned_result():
309356
if not isinstance(error_type, NullType):
@@ -319,9 +366,9 @@ def main():
319366
# identifiable:
320367
functions_to_patch.append((interface, function))
321368

322-
print(generate_exception_classes(exceptions_to_generate.keys()))
323-
generate_function_patches(functions_to_patch)
324-
369+
write_files(exception_code_tree(exceptions_to_generate.keys()),
370+
Path(__file__).parent.parent / "fastly_compute" / "exceptions")
371+
#generate_function_patches(functions_to_patch)
325372

326373

327374
if __name__ == "__main__":

0 commit comments

Comments
 (0)