Skip to content

Commit 64496b4

Browse files
committed
Swift: cleanup and some docstrings for codegen
Also added code generation and clang formatting to the pre-commit configuration.
1 parent 91fd83a commit 64496b4

File tree

12 files changed

+186
-92
lines changed

12 files changed

+186
-92
lines changed

.pre-commit-config.yaml

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,41 @@
22
# See https://pre-commit.com/hooks.html for more hooks
33
exclude: /test/.*$(?<!\.ql)(?<!\.qll)(?<!\.qlref)
44
repos:
5-
- repo: https://github.com/pre-commit/pre-commit-hooks
5+
- repo: https://github.com/pre-commit/pre-commit-hooks
66
rev: v3.2.0
77
hooks:
8-
- id: trailing-whitespace
9-
- id: end-of-file-fixer
8+
- id: trailing-whitespace
9+
- id: end-of-file-fixer
1010

11-
- repo: local
11+
- repo: https://github.com/pre-commit/mirrors-clang-format
12+
rev: v13.0.1
1213
hooks:
13-
- id: codeql-format
14+
- id: clang-format
15+
files: ^swift/.*\.(h|c|cpp)$
16+
17+
- repo: local
18+
hooks:
19+
- id: codeql-format
1420
name: Fix QL file formatting
1521
files: \.qll?$
1622
language: system
1723
entry: codeql query format --in-place
1824

19-
- id: sync-files
25+
- id: sync-files
2026
name: Fix files required to be identical
2127
language: system
2228
entry: python3 config/sync-files.py --latest
2329
pass_filenames: false
2430

25-
- id: qhelp
31+
- id: qhelp
2632
name: Check query help generation
2733
files: \.qhelp$
2834
language: system
2935
entry: python3 misc/scripts/check-qhelp.py
36+
37+
- id: swift-codegen
38+
name: Run Swift checked in code generation
39+
files: ^swift/(codegen/|.*/generated/|ql/lib/swift\.dbscheme$)
40+
language: system
41+
entry: bazel run //swift/codegen
42+
pass_filenames: false

swift/codegen/BUILD.bazel

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
11
py_binary(
22
name = "codegen",
33
srcs = glob(["**/*.py"]),
4-
data = glob(["**/*.mustache"]) + [
5-
"schema.yml",
6-
"prefix.dbscheme",
7-
],
84
)

swift/codegen/codegen.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#!/usr/bin/env python3
2+
""" Driver script to run all checked in code generation """
23

34
from lib import generator
45
import dbschemegen

swift/codegen/dbschemegen.py

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,30 @@
11
#!/usr/bin/env python3
2+
import pathlib
23

34
import inflection
45

5-
from lib.renderer import Renderer
6-
from lib.dbscheme import *
76
from lib import paths, schema, generator
7+
from lib.dbscheme import *
88

99
log = logging.getLogger(__name__)
1010

1111

1212
def dbtype(typename):
13+
""" translate a type to a dbscheme counterpart, using `@lower_underscore` format for classes """
1314
if typename[0].isupper():
1415
return "@" + inflection.underscore(typename)
1516
return typename
1617

1718

1819
def cls_to_dbscheme(cls: schema.Class):
20+
""" Yield all dbscheme entities needed to model class `cls` """
1921
if cls.derived:
2022
yield DbUnion(dbtype(cls.name), (dbtype(c) for c in cls.derived))
21-
if not cls.derived or any(f.is_single() for f in cls.fields):
23+
# output a table specific to a class only if it is a leaf class or it has 1-to-1 properties
24+
# Leaf classes need a table to bind the `@` ids
25+
# 1-to-1 properties are added to a class specific table
26+
# in other cases, separate tables are used for the properties, and a class specific table is unneeded
27+
if not cls.derived or any(f.is_single for f in cls.properties):
2228
binding = not cls.derived
2329
keyset = DbKeySet(["id"]) if cls.derived else None
2430
yield DbTable(
@@ -27,11 +33,12 @@ def cls_to_dbscheme(cls: schema.Class):
2733
columns=[
2834
DbColumn("id", type=dbtype(cls.name), binding=binding),
2935
] + [
30-
DbColumn(f.name, dbtype(f.type)) for f in cls.fields if f.is_single()
36+
DbColumn(f.name, dbtype(f.type)) for f in cls.properties if f.is_single
3137
]
3238
)
33-
for f in cls.fields:
34-
if f.is_optional():
39+
# use property-specific tables for 1-to-many and 1-to-at-most-1 properties
40+
for f in cls.properties:
41+
if f.is_optional:
3542
yield DbTable(
3643
keyset=DbKeySet(["id"]),
3744
name=inflection.tableize(f"{cls.name}_{f.name}"),
@@ -40,7 +47,7 @@ def cls_to_dbscheme(cls: schema.Class):
4047
DbColumn(f.name, dbtype(f.type)),
4148
],
4249
)
43-
elif f.is_repeated():
50+
elif f.is_repeated:
4451
yield DbTable(
4552
keyset=DbKeySet(["id", "index"]),
4653
name=inflection.tableize(f"{cls.name}_{f.name}"),
@@ -52,24 +59,31 @@ def cls_to_dbscheme(cls: schema.Class):
5259
)
5360

5461

55-
def generate(opts):
62+
def get_declarations(data: schema.Schema):
63+
return [d for cls in data.classes.values() for d in cls_to_dbscheme(cls)]
64+
65+
66+
def get_includes(data: schema.Schema, include_dir: pathlib.Path):
67+
includes = []
68+
for inc in data.includes:
69+
inc = include_dir / inc
70+
with open(inc) as inclusion:
71+
includes.append(DbSchemeInclude(src=inc.relative_to(paths.swift_dir), data=inclusion.read()))
72+
return includes
73+
74+
75+
def generate(opts, renderer):
5676
input = opts.schema.resolve()
5777
out = opts.dbscheme.resolve()
58-
renderer = Renderer(opts.check)
5978

6079
with open(input) as src:
6180
data = schema.load(src)
6281

63-
declarations = [d for cls in data.classes.values() for d in cls_to_dbscheme(cls)]
82+
dbscheme = DbScheme(src=input.relative_to(paths.swift_dir),
83+
includes=get_includes(data, include_dir=input.parent),
84+
declarations=get_declarations(data))
6485

65-
includes = []
66-
for inc in data.includes:
67-
inc = input.parent / inc
68-
with open(inc) as inclusion:
69-
includes.append({"src": inc.relative_to(paths.swift_dir), "data": inclusion.read()})
70-
renderer.render("dbscheme", out, includes=includes, src=input.relative_to(paths.swift_dir),
71-
declarations=declarations)
72-
return renderer.written
86+
renderer.render("dbscheme", out, dbscheme)
7387

7488

7589
if __name__ == "__main__":

swift/codegen/lib/dbscheme.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
""" dbscheme format representation """
2+
13
import logging
2-
import re
34
from dataclasses import dataclass
45
from typing import ClassVar, List
56

@@ -83,3 +84,16 @@ def __post_init__(self):
8384
self.rhs = [DbUnionCase(x) for x in self.rhs]
8485
self.rhs.sort(key=lambda c: c.type)
8586
self.rhs[0].first = True
87+
88+
89+
@dataclass
90+
class DbSchemeInclude:
91+
src: str
92+
data: str
93+
94+
95+
@dataclass
96+
class DbScheme:
97+
src: str
98+
includes: List[DbSchemeInclude]
99+
declarations: List[DbDecl]

swift/codegen/lib/generator.py

Lines changed: 15 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,29 @@
1+
""" generator script scaffolding """
2+
13
import argparse
2-
import collections
34
import logging
4-
import pathlib
55
import sys
66

7-
from . import paths
8-
9-
options = collections.defaultdict(list)
10-
11-
12-
class Option:
13-
def __init__(self, *args, tags=None, **kwargs):
14-
tags = tags or []
15-
self.args = args
16-
self.kwargs = kwargs
17-
if tags:
18-
for t in tags:
19-
options[t].append(self)
20-
else:
21-
options["*"].append(self)
7+
from . import options, render
228

23-
def add_to(self, parser: argparse.ArgumentParser):
24-
parser.add_argument(*self.args, **self.kwargs)
259

26-
27-
Option("--check", "-c", action="store_true")
28-
Option("--verbose", "-v", action="store_true")
29-
Option("--schema", tags=["schema"], type=pathlib.Path, default=paths.swift_dir / "codegen/schema.yml")
30-
Option("--dbscheme", tags=["dbscheme"], type=pathlib.Path, default=paths.swift_dir / "ql/lib/swift.dbscheme")
31-
32-
33-
def _parse(*tags):
10+
def _parse(tags):
3411
parser = argparse.ArgumentParser()
35-
if not tags:
36-
opts = [o for os in options.values() for o in os]
37-
else:
38-
opts = options["*"]
39-
for t in tags:
40-
opts.extend(options[t])
41-
for opt in opts:
12+
for opt in options.get(tags):
4213
opt.add_to(parser)
4314
ret = parser.parse_args()
4415
log_level = logging.DEBUG if ret.verbose else logging.INFO
4516
logging.basicConfig(format="{levelname} {message}", style='{', level=log_level)
4617
return ret
4718

4819

49-
def run(*generate, tags=()):
50-
opts = _parse(*tags)
51-
done_something = False
52-
for g in generate:
53-
if g(opts):
54-
done_something = True
55-
sys.exit(1 if opts.check and done_something else 0)
20+
def run(*generators, tags=None):
21+
""" run generation functions in `generators`, parsing options tagged with `tags` (all if unspecified)
22+
23+
`generators` should be callables taking as input an option namespace and a `render.Renderer` instance
24+
"""
25+
opts = _parse(tags)
26+
renderer = render.Renderer(dryrun=opts.check)
27+
for g in generators:
28+
g(opts, renderer)
29+
sys.exit(1 if opts.check and renderer.done_something else 0)

swift/codegen/lib/options.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
""" generator options, categorized by tags """
2+
3+
import argparse
4+
import collections
5+
import pathlib
6+
from typing import Tuple
7+
8+
from . import paths
9+
10+
11+
def _init_options():
12+
Option("--check", "-c", action="store_true")
13+
Option("--verbose", "-v", action="store_true")
14+
Option("--schema", tags=["schema"], type=pathlib.Path, default=paths.swift_dir / "codegen/schema.yml")
15+
Option("--dbscheme", tags=["dbscheme"], type=pathlib.Path, default=paths.swift_dir / "ql/lib/swift.dbscheme")
16+
17+
18+
_options = collections.defaultdict(list)
19+
20+
21+
class Option:
22+
def __init__(self, *args, tags=None, **kwargs):
23+
tags = tags or []
24+
self.args = args
25+
self.kwargs = kwargs
26+
if tags:
27+
for t in tags:
28+
_options[t].append(self)
29+
else:
30+
_options["*"].append(self)
31+
32+
def add_to(self, parser: argparse.ArgumentParser):
33+
parser.add_argument(*self.args, **self.kwargs)
34+
35+
36+
_init_options()
37+
38+
39+
def get(tags: Tuple[str]):
40+
""" get options marked by `tags`
41+
42+
Return all options if tags is falsy. Options tagged by wildcard '*' are always returned
43+
"""
44+
if not tags:
45+
return (o for tagged_opts in _options.values() for o in tagged_opts)
46+
else:
47+
# use specifically tagged options + those tagged with wildcard *
48+
return (o for tag in ('*',) + tags for o in _options[tag])

swift/codegen/lib/paths.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
""" module providing useful filesystem paths """
2+
13
import pathlib
24
import sys
35
import os
46

57
try:
6-
_workspace_dir = pathlib.Path(os.environ['BUILD_WORKSPACE_DIRECTORY'])
8+
_workspace_dir = pathlib.Path(os.environ['BUILD_WORKSPACE_DIRECTORY']) # <- means we are using bazel run
79
swift_dir = _workspace_dir / 'swift'
810
lib_dir = swift_dir / 'codegen' / 'lib'
911
except KeyError:

swift/codegen/lib/renderer.py renamed to swift/codegen/lib/render.py

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
""" template renderer module, wrapping around `pystache.Renderer`
2+
3+
`pystache` is a python mustache engine, and mustache is a template language. More information on
4+
5+
https://mustache.github.io/
6+
"""
7+
18
import hashlib
29
import logging
310

@@ -8,15 +15,18 @@
815
log = logging.getLogger(__name__)
916

1017

11-
def md5(data):
18+
def _md5(data):
1219
return hashlib.md5(data).digest()
1320

1421

1522
class Renderer:
16-
def __init__(self, check=False):
23+
""" Template renderer using mustache templates in the `templates` directory """
24+
25+
def __init__(self, dryrun=False):
26+
""" Construct the renderer, which will not write anything if `dryrun` is `True` """
1727
self.r = pystache.Renderer(search_dirs=str(paths.lib_dir / "templates"), escape=lambda u: u)
1828
self.generator = paths.exe_file.relative_to(paths.swift_dir)
19-
self.check = check
29+
self.dryrun = dryrun
2030
self.written = set()
2131
self.skipped = set()
2232
self.erased = set()
@@ -29,18 +39,21 @@ def done_something(self):
2939
def rendered(self):
3040
return self.written | self.skipped
3141

32-
def render(self, name, output, **data):
42+
def render(self, name, output, data):
43+
""" Render the template called `name` in the template directory, writing to `output` using `data` as context
44+
45+
If the file is unchanged, then no write is performed (and `done_something` remains unchanged)
46+
"""
3347
mnemonic, _, _ = name.lower().partition(".")
3448
output.parent.mkdir(parents=True, exist_ok=True)
35-
data["generator"] = self.generator
36-
data = self.r.render_name(name, data)
49+
data = self.r.render_name(name, data, generator=self.generator)
3750
if output.is_file():
3851
with open(output, "rb") as file:
39-
if md5(data.encode()) == md5(file.read()):
52+
if _md5(data.encode()) == _md5(file.read()):
4053
log.debug(f"skipped {output.name}")
4154
self.skipped.add(output)
4255
return
43-
if self.check:
56+
if self.dryrun:
4457
log.error(f"would have generated {mnemonic} {output.name}")
4558
else:
4659
with open(output, "w") as out:
@@ -49,9 +62,10 @@ def render(self, name, output, **data):
4962
self.written.add(output)
5063

5164
def cleanup(self, existing):
65+
""" Remove files in `existing` for which no `render` has been called """
5266
for f in existing - self.written - self.skipped:
5367
if f.is_file():
54-
if self.check:
68+
if self.dryrun:
5569
log.error(f"would have removed {f.name}")
5670
else:
5771
f.unlink()

0 commit comments

Comments
 (0)