Skip to content

Commit 1f21d75

Browse files
authored
Merge pull request github#17533 from github/redsun82/codegen-parametrized-pragmas
Codegen: introduce inherited pragmas and move remaining decorations
2 parents f4071dd + 4e59fa9 commit 1f21d75

File tree

10 files changed

+101
-99
lines changed

10 files changed

+101
-99
lines changed

misc/codegen/generators/qlgen.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ def get_ql_property(cls: schema.Class, prop: schema.Property, lookup: typing.Dic
115115
is_unordered=prop.is_unordered,
116116
description=prop.description,
117117
synth=bool(cls.synth) or prop.synth,
118-
type_is_hideable=lookup[prop.type].hideable if prop.type in lookup else False,
118+
type_is_hideable="ql_hideable" in lookup[prop.type].pragmas if prop.type in lookup else False,
119119
internal="ql_internal" in prop.pragmas,
120120
)
121121
if prop.is_single:
@@ -154,7 +154,6 @@ def get_ql_property(cls: schema.Class, prop: schema.Property, lookup: typing.Dic
154154

155155

156156
def get_ql_class(cls: schema.Class, lookup: typing.Dict[str, schema.Class]) -> ql.Class:
157-
pragmas = {k: True for k in cls.pragmas if k.startswith("qltest")}
158157
prev_child = ""
159158
properties = []
160159
for p in cls.properties:
@@ -170,9 +169,8 @@ def get_ql_class(cls: schema.Class, lookup: typing.Dict[str, schema.Class]) -> q
170169
properties=properties,
171170
dir=pathlib.Path(cls.group or ""),
172171
doc=cls.doc,
173-
hideable=cls.hideable,
172+
hideable="ql_hideable" in cls.pragmas,
174173
internal="ql_internal" in cls.pragmas,
175-
**pragmas,
176174
)
177175

178176

@@ -448,7 +446,8 @@ def generate(opts, renderer):
448446
for c in data.classes.values():
449447
if should_skip_qltest(c, data.classes):
450448
continue
451-
test_with = data.classes[c.test_with] if c.test_with else c
449+
test_with_name = c.pragmas.get("qltest_test_with")
450+
test_with = data.classes[test_with_name] if test_with_name else c
452451
test_dir = test_out / test_with.group / test_with.name
453452
test_dir.mkdir(parents=True, exist_ok=True)
454453
if all(f.suffix in (".txt", ".ql", ".actual", ".expected") for f in test_dir.glob("*.*")):

misc/codegen/generators/rusttestgen.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ def generate(opts, renderer):
6060
if fn:
6161
indent = 4 * " "
6262
code = [indent + l for l in code]
63-
test_with = schema.classes[cls.test_with] if cls.test_with else cls
63+
test_with_name = typing.cast(str, cls.pragmas.get("qltest_test_with"))
64+
test_with = schema.classes[test_with_name] if test_with_name else cls
6465
test = opts.ql_test_output / test_with.group / test_with.name / f"gen_{test_name}.rs"
6566
renderer.render(TestCode(code="\n".join(code), function=fn), test)

misc/codegen/lib/ql.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,6 @@ class Class:
107107
dir: pathlib.Path = pathlib.Path()
108108
imports: List[str] = field(default_factory=list)
109109
import_prefix: Optional[str] = None
110-
qltest_skip: bool = False
111-
qltest_collapse_hierarchy: bool = False
112-
qltest_uncollapse_hierarchy: bool = False
113110
internal: bool = False
114111
doc: List[str] = field(default_factory=list)
115112
hideable: bool = False

misc/codegen/lib/schema.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,8 @@ class Class:
9191
bases: List[str] = field(default_factory=list)
9292
derived: Set[str] = field(default_factory=set)
9393
properties: List[Property] = field(default_factory=list)
94-
group: str = ""
9594
pragmas: List[str] | Dict[str, object] = field(default_factory=dict)
9695
doc: List[str] = field(default_factory=list)
97-
hideable: bool = False
98-
test_with: Optional[str] = None
9996

10097
def __post_init__(self):
10198
if not isinstance(self.pragmas, dict):
@@ -118,7 +115,7 @@ def check_types(self, known: typing.Iterable[str]):
118115
if synth.on_arguments is not None:
119116
for t in synth.on_arguments.values():
120117
_check_type(t, known)
121-
_check_type(self.test_with, known)
118+
_check_type(self.pragmas.get("qltest_test_with"), known)
122119

123120
@property
124121
def synth(self) -> SynthInfo | bool | None:
@@ -127,6 +124,10 @@ def synth(self) -> SynthInfo | bool | None:
127124
def mark_synth(self):
128125
self.pragmas.setdefault("synth", True)
129126

127+
@property
128+
def group(self) -> str:
129+
return typing.cast(str, self.pragmas.get("group", ""))
130+
130131

131132
@dataclass
132133
class Schema:
@@ -211,3 +212,6 @@ def split_doc(doc):
211212
while trimmed and not trimmed[0]:
212213
trimmed.pop(0)
213214
return trimmed
215+
216+
217+
inheritable_pragma_prefix = "_inheritable_pragma_"

misc/codegen/lib/schemadefs.py

Lines changed: 29 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
from misc.codegen.lib.schema import Property
77

8+
_set = set
9+
810

911
@_dataclass
1012
class _ChildModifier(_schema.PropertyModifier):
@@ -79,7 +81,7 @@ class _SynthModifier(_schema.PropertyModifier, _Namespace):
7981
def modify(self, prop: _schema.Property):
8082
prop.synth = self.synth
8183

82-
def negate(self) -> "PropertyModifier":
84+
def negate(self) -> _schema.PropertyModifier:
8385
return _SynthModifier(self.name, False)
8486

8587

@@ -100,14 +102,18 @@ class _ClassPragma(_PragmaBase):
100102
""" A class pragma.
101103
For schema classes it acts as a python decorator with `@`.
102104
"""
105+
inherited: bool = False
103106
value: object = None
104107

105108
def __call__(self, cls: type) -> type:
106109
""" use this pragma as a decorator on classes """
107-
# not using hasattr as we don't want to land on inherited pragmas
108-
if "_pragmas" not in cls.__dict__:
109-
cls._pragmas = {}
110-
self._apply(cls._pragmas)
110+
if self.inherited:
111+
setattr(cls, f"{_schema.inheritable_pragma_prefix}{self.pragma}", self.value)
112+
else:
113+
# not using hasattr as we don't want to land on inherited pragmas
114+
if "_pragmas" not in cls.__dict__:
115+
cls._pragmas = {}
116+
self._apply(cls._pragmas)
111117
return cls
112118

113119
def _apply(self, pragmas: _Dict[str, object]) -> None:
@@ -125,7 +131,7 @@ class _Pragma(_ClassPragma, _schema.PropertyModifier):
125131
def modify(self, prop: _schema.Property):
126132
self._apply(prop.pragmas)
127133

128-
def negate(self) -> "PropertyModifier":
134+
def negate(self) -> _schema.PropertyModifier:
129135
return _Pragma(self.pragma, remove=True)
130136

131137
def _apply(self, pragmas: _Dict[str, object]) -> None:
@@ -142,13 +148,14 @@ class _ParametrizedClassPragma(_PragmaBase):
142148
"""
143149
_pragma_class: _ClassVar[type] = _ClassPragma
144150

145-
function: _Callable[..., object] = None
151+
inherited: bool = False
152+
factory: _Callable[..., object] = None
146153

147154
def __post_init__(self):
148-
self.__signature__ = _inspect.signature(self.function).replace(return_annotation=self._pragma_class)
155+
self.__signature__ = _inspect.signature(self.factory).replace(return_annotation=self._pragma_class)
149156

150157
def __call__(self, *args, **kwargs) -> _pragma_class:
151-
return self._pragma_class(self.pragma, value=self.function(*args, **kwargs))
158+
return self._pragma_class(self.pragma, self.inherited, value=self.factory(*args, **kwargs))
152159

153160

154161
@_dataclass
@@ -204,15 +211,6 @@ def __getitem__(self, item):
204211
_ClassDecorator = _Callable[[type], type]
205212

206213

207-
def _annotate(**kwargs) -> _ClassDecorator:
208-
def f(cls: type) -> type:
209-
for k, v in kwargs.items():
210-
setattr(cls, f"_{k}", v)
211-
return cls
212-
213-
return f
214-
215-
216214
boolean = "boolean"
217215
int = "int"
218216
string = "string"
@@ -226,31 +224,29 @@ def f(cls: type) -> type:
226224
doc = _DocModifier
227225
desc = _DescModifier
228226

229-
use_for_null = _annotate(null=True)
227+
use_for_null = _ClassPragma("null")
230228

231229
qltest.add(_Pragma("skip"))
232230
qltest.add(_ClassPragma("collapse_hierarchy"))
233231
qltest.add(_ClassPragma("uncollapse_hierarchy"))
234-
qltest.test_with = lambda cls: _annotate(test_with=cls) # inheritable
232+
qltest.add(_ParametrizedClassPragma("test_with", inherited=True, factory=_schema.get_type_name))
235233

236-
ql.add(_ParametrizedClassPragma("default_doc_name", lambda doc: doc))
237-
ql.hideable = _annotate(hideable=True) # inheritable
234+
ql.add(_ParametrizedClassPragma("default_doc_name", factory=lambda doc: doc))
235+
ql.add(_ClassPragma("hideable", inherited=True))
238236
ql.add(_Pragma("internal"))
239237

240238
cpp.add(_Pragma("skip"))
241239

242240
rust.add(_Pragma("skip_doc_test"))
243241

244-
rust.add(_ParametrizedClassPragma("doc_test_signature", lambda signature: signature))
242+
rust.add(_ParametrizedClassPragma("doc_test_signature", factory=lambda signature: signature))
245243

244+
group = _ParametrizedClassPragma("group", inherited=True, factory=lambda group: group)
246245

247-
def group(name: str = "") -> _ClassDecorator:
248-
return _annotate(group=name)
249246

250-
251-
synth.add(_ParametrizedClassPragma("from_class", lambda ref: _schema.SynthInfo(
247+
synth.add(_ParametrizedClassPragma("from_class", factory=lambda ref: _schema.SynthInfo(
252248
from_class=_schema.get_type_name(ref))), key="synth")
253-
synth.add(_ParametrizedClassPragma("on_arguments", lambda **kwargs:
249+
synth.add(_ParametrizedClassPragma("on_arguments", factory=lambda **kwargs:
254250
_schema.SynthInfo(on_arguments={k: _schema.get_type_name(t) for k, t in kwargs.items()})), key="synth")
255251

256252

@@ -288,16 +284,11 @@ def decorator(cls: type) -> _PropertyAnnotation:
288284
raise _schema.Error("Annotation classes must be named _")
289285
if cls.__doc__ is not None:
290286
annotated_cls.__doc__ = cls.__doc__
291-
old_pragmas = getattr(annotated_cls, "_pragmas", None)
292-
new_pragmas = getattr(cls, "_pragmas", {})
293-
if old_pragmas:
294-
old_pragmas.update(new_pragmas)
295-
else:
296-
annotated_cls._pragmas = new_pragmas
297-
for a, v in cls.__dict__.items():
298-
# transfer annotations
299-
if a.startswith("_") and not a.startswith("__") and a != "_pragmas":
300-
setattr(annotated_cls, a, v)
287+
for p, v in cls.__dict__.get("_pragmas", {}).items():
288+
_ClassPragma(p, value=v)(annotated_cls)
289+
for a in dir(cls):
290+
if a.startswith(_schema.inheritable_pragma_prefix):
291+
setattr(annotated_cls, a, getattr(cls, a))
301292
for p, a in cls.__annotations__.items():
302293
if p in annotated_cls.__annotations__:
303294
annotated_cls.__annotations__[p] |= a

misc/codegen/loaders/schemaloader.py

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -37,19 +37,23 @@ def _get_class(cls: type) -> schema.Class:
3737
if cls.__name__ != to_underscore_and_back:
3838
raise schema.Error(f"Class name must be upper camel-case, without capitalized acronyms, found {cls.__name__} "
3939
f"instead of {to_underscore_and_back}")
40-
if len({b._group for b in cls.__bases__ if hasattr(b, "_group")}) > 1:
40+
if len({g for g in (getattr(b, f"{schema.inheritable_pragma_prefix}group", None)
41+
for b in cls.__bases__) if g}) > 1:
4142
raise schema.Error(f"Bases with mixed groups for {cls.__name__}")
42-
if any(getattr(b, "_null", False) for b in cls.__bases__):
43+
pragmas = {
44+
# dir and getattr inherit from bases
45+
a[len(schema.inheritable_pragma_prefix):]: getattr(cls, a)
46+
for a in dir(cls) if a.startswith(schema.inheritable_pragma_prefix)
47+
}
48+
pragmas |= cls.__dict__.get("_pragmas", {})
49+
derived = {d.__name__ for d in cls.__subclasses__()}
50+
if "null" in pragmas and derived:
4351
raise schema.Error(f"Null class cannot be derived")
4452
return schema.Class(name=cls.__name__,
4553
bases=[b.__name__ for b in cls.__bases__ if b is not object],
46-
derived={d.__name__ for d in cls.__subclasses__()},
47-
# getattr to inherit from bases
48-
group=getattr(cls, "_group", ""),
49-
hideable=getattr(cls, "_hideable", False),
50-
test_with=_get_name(getattr(cls, "_test_with", None)),
54+
derived=derived,
55+
pragmas=pragmas,
5156
# in the following we don't use `getattr` to avoid inheriting
52-
pragmas=cls.__dict__.get("_pragmas", {}),
5357
properties=[
5458
a | _PropertyNamer(n)
5559
for n, a in cls.__dict__.get("__annotations__", {}).items()
@@ -105,21 +109,23 @@ def fill_is_synth(name: str):
105109

106110
def _fill_hideable_information(classes: typing.Dict[str, schema.Class]):
107111
""" Update the class map propagating the `hideable` attribute upwards in the hierarchy """
108-
todo = [cls for cls in classes.values() if cls.hideable]
112+
todo = [cls for cls in classes.values() if "ql_hideable" in cls.pragmas]
109113
while todo:
110114
cls = todo.pop()
111115
for base in cls.bases:
112116
supercls = classes[base]
113-
if not supercls.hideable:
114-
supercls.hideable = True
117+
if "ql_hideable" not in supercls.pragmas:
118+
supercls.pragmas["ql_hideable"] = None
115119
todo.append(supercls)
116120

117121

118122
def _check_test_with(classes: typing.Dict[str, schema.Class]):
119123
for cls in classes.values():
120-
if cls.test_with is not None and classes[cls.test_with].test_with is not None:
121-
raise schema.Error(f"{cls.name} has test_with {cls.test_with} which in turn "
122-
f"has test_with {classes[cls.test_with].test_with}, use that directly")
124+
test_with = typing.cast(str, cls.pragmas.get("qltest_test_with"))
125+
transitive_test_with = test_with and classes[test_with].pragmas.get("qltest_test_with")
126+
if test_with and transitive_test_with:
127+
raise schema.Error(f"{cls.name} has test_with {test_with} which in turn "
128+
f"has test_with {transitive_test_with}, use that directly")
123129

124130

125131
def load(m: types.ModuleType) -> schema.Schema:
@@ -145,11 +151,11 @@ def load(m: types.ModuleType) -> schema.Schema:
145151
f"Only one root class allowed, found second root {name}")
146152
cls.check_types(known)
147153
classes[name] = cls
148-
if getattr(data, "_null", False):
154+
if "null" in cls.pragmas:
155+
del cls.pragmas["null"]
149156
if null is not None:
150157
raise schema.Error(f"Null class {null} already defined, second null class {name} not allowed")
151158
null = name
152-
cls.is_null_class = True
153159

154160
_fill_synth_information(classes)
155161
_fill_hideable_information(classes)

misc/codegen/test/test_cppgen.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -156,10 +156,10 @@ def test_classes_with_dirs(generate_grouped):
156156
cbase = cpp.Class(name="CBase")
157157
assert generate_grouped([
158158
schema.Class(name="A"),
159-
schema.Class(name="B", group="foo"),
160-
schema.Class(name="CBase", derived={"C"}, group="bar"),
161-
schema.Class(name="C", bases=["CBase"], group="bar"),
162-
schema.Class(name="D", group="foo/bar/baz"),
159+
schema.Class(name="B", pragmas={"group": "foo"}),
160+
schema.Class(name="CBase", derived={"C"}, pragmas={"group": "bar"}),
161+
schema.Class(name="C", bases=["CBase"], pragmas={"group": "bar"}),
162+
schema.Class(name="D", pragmas={"group": "foo/bar/baz"}),
163163
]) == {
164164
".": [cpp.Class(name="A", trap_name="As", final=True)],
165165
"foo": [cpp.Class(name="B", trap_name="Bs", final=True)],

0 commit comments

Comments
 (0)