Skip to content

Commit 1bffc2a

Browse files
authored
Merge pull request #17532 from github/redsun82/codegen-parametrized-pragmas
Codegen: parametrized pragmas
2 parents 594045b + db00cb6 commit 1bffc2a

File tree

9 files changed

+147
-96
lines changed

9 files changed

+147
-96
lines changed

misc/codegen/generators/qlgen.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ def _get_doc(cls: schema.Class, prop: schema.Property, plural=None):
9696
return format.format(**{noun: transform(noun) for noun in nouns})
9797

9898
prop_name = _humanize(prop.name)
99-
class_name = cls.default_doc_name or _humanize(inflection.underscore(cls.name))
99+
class_name = cls.pragmas.get("ql_default_doc_name", _humanize(inflection.underscore(cls.name)))
100100
if prop.is_predicate:
101101
return f"this {class_name} {prop_name}"
102102
if plural is not None:

misc/codegen/generators/rusttestgen.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def generate(opts, renderer):
5555
continue
5656
assert not adding_code, "Unterminated code block in docstring: " + "\n".join(cls.doc)
5757
test_name = inflection.underscore(cls.name)
58-
signature = cls.rust_doc_test_function
58+
signature = cls.pragmas.get("rust_doc_test_signature", "() -> ()")
5959
fn = signature and Function(f"test_{test_name}", signature)
6060
if fn:
6161
indent = 4 * " "

misc/codegen/lib/schema.py

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,15 @@ class Kind(Enum):
3232
name: Optional[str] = None
3333
type: Optional[str] = None
3434
is_child: bool = False
35-
pragmas: List[str] = field(default_factory=list)
35+
pragmas: List[str] | Dict[str, object] = field(default_factory=dict)
3636
doc: Optional[str] = None
3737
description: List[str] = field(default_factory=list)
3838
synth: bool = False
3939

40+
def __post_init__(self):
41+
if not isinstance(self.pragmas, dict):
42+
self.pragmas = dict.fromkeys(self.pragmas, None)
43+
4044
@property
4145
def is_single(self) -> bool:
4246
return self.kind == self.Kind.SINGLE
@@ -88,14 +92,14 @@ class Class:
8892
derived: Set[str] = field(default_factory=set)
8993
properties: List[Property] = field(default_factory=list)
9094
group: str = ""
91-
pragmas: List[str] = field(default_factory=list)
92-
synth: Optional[Union[SynthInfo, bool]] = None
93-
"""^^^ filled with `True` for non-final classes with only synthesized final descendants """
95+
pragmas: List[str] | Dict[str, object] = field(default_factory=dict)
9496
doc: List[str] = field(default_factory=list)
95-
default_doc_name: Optional[str] = None
9697
hideable: bool = False
9798
test_with: Optional[str] = None
98-
rust_doc_test_function: Optional["FunctionInfo"] = "() -> ()" # TODO: parametrized pragmas
99+
100+
def __post_init__(self):
101+
if not isinstance(self.pragmas, dict):
102+
self.pragmas = dict.fromkeys(self.pragmas, None)
99103

100104
@property
101105
def final(self):
@@ -108,13 +112,21 @@ def check_types(self, known: typing.Iterable[str]):
108112
_check_type(d, known)
109113
for p in self.properties:
110114
_check_type(p.type, known)
111-
if self.synth is not None:
112-
_check_type(self.synth.from_class, known)
113-
if self.synth.on_arguments is not None:
114-
for t in self.synth.on_arguments.values():
115+
if "synth" in self.pragmas:
116+
synth = self.pragmas["synth"]
117+
_check_type(synth.from_class, known)
118+
if synth.on_arguments is not None:
119+
for t in synth.on_arguments.values():
115120
_check_type(t, known)
116121
_check_type(self.test_with, known)
117122

123+
@property
124+
def synth(self) -> SynthInfo | bool | None:
125+
return self.pragmas.get("synth")
126+
127+
def mark_synth(self):
128+
self.pragmas.setdefault("synth", True)
129+
118130

119131
@dataclass
120132
class Schema:

misc/codegen/lib/schemadefs.py

Lines changed: 83 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Callable as _Callable, List as _List
1+
from typing import Callable as _Callable, Dict as _Dict, ClassVar as _ClassVar
22
from misc.codegen.lib import schema as _schema
33
import inspect as _inspect
44
from dataclasses import dataclass as _dataclass
@@ -62,11 +62,14 @@ def include(source: str):
6262
_inspect.currentframe().f_back.f_locals.setdefault("includes", []).append(source)
6363

6464

65+
@_dataclass
6566
class _Namespace:
6667
""" simple namespacing mechanism """
68+
name: str
6769

68-
def __init__(self, **kwargs):
69-
self.__dict__.update(kwargs)
70+
def add(self, pragma: "_PragmaBase", key: str | None = None):
71+
self.__dict__[pragma.pragma] = pragma
72+
pragma.pragma = key or f"{self.name}_{pragma.pragma}"
7073

7174

7275
@_dataclass
@@ -77,51 +80,86 @@ def modify(self, prop: _schema.Property):
7780
prop.synth = self.synth
7881

7982
def negate(self) -> "PropertyModifier":
80-
return _SynthModifier(False)
83+
return _SynthModifier(self.name, False)
84+
85+
86+
qltest = _Namespace("qltest")
87+
ql = _Namespace("ql")
88+
cpp = _Namespace("cpp")
89+
rust = _Namespace("rust")
90+
synth = _SynthModifier("synth")
91+
92+
93+
@_dataclass
94+
class _PragmaBase:
95+
pragma: str
96+
97+
98+
@_dataclass
99+
class _ClassPragma(_PragmaBase):
100+
""" A class pragma.
101+
For schema classes it acts as a python decorator with `@`.
102+
"""
103+
value: object = None
81104

105+
def __call__(self, cls: type) -> type:
106+
""" 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)
111+
return cls
82112

83-
qltest = _Namespace()
84-
ql = _Namespace()
85-
cpp = _Namespace()
86-
rust = _Namespace()
87-
synth = _SynthModifier()
113+
def _apply(self, pragmas: _Dict[str, object]) -> None:
114+
pragmas[self.pragma] = self.value
88115

89116

90117
@_dataclass
91-
class _Pragma(_schema.PropertyModifier):
118+
class _Pragma(_ClassPragma, _schema.PropertyModifier):
92119
""" A class or property pragma.
93120
For properties, it functions similarly to a `_PropertyModifier` with `|`, adding the pragma.
94121
For schema classes it acts as a python decorator with `@`.
95122
"""
96-
pragma: str
97123
remove: bool = False
98124

99-
def __post_init__(self):
100-
namespace, _, name = self.pragma.partition('_')
101-
setattr(globals()[namespace], name, self)
102-
103125
def modify(self, prop: _schema.Property):
104126
self._apply(prop.pragmas)
105127

106128
def negate(self) -> "PropertyModifier":
107129
return _Pragma(self.pragma, remove=True)
108130

109-
def __call__(self, cls: type) -> type:
110-
""" use this pragma as a decorator on classes """
111-
if "_pragmas" in cls.__dict__: # not using hasattr as we don't want to land on inherited pragmas
112-
self._apply(cls._pragmas)
113-
elif not self.remove:
114-
cls._pragmas = [self.pragma]
115-
return cls
116-
117-
def _apply(self, pragmas: _List[str]) -> None:
131+
def _apply(self, pragmas: _Dict[str, object]) -> None:
118132
if self.remove:
119-
try:
120-
pragmas.remove(self.pragma)
121-
except ValueError:
122-
pass
133+
pragmas.pop(self.pragma, None)
123134
else:
124-
pragmas.append(self.pragma)
135+
super()._apply(pragmas)
136+
137+
138+
@_dataclass
139+
class _ParametrizedClassPragma(_PragmaBase):
140+
""" A class parametrized pragma.
141+
Needs to be applied to a parameter to give a class pragma.
142+
"""
143+
_pragma_class: _ClassVar[type] = _ClassPragma
144+
145+
function: _Callable[..., object] = None
146+
147+
def __post_init__(self):
148+
self.__signature__ = _inspect.signature(self.function).replace(return_annotation=self._pragma_class)
149+
150+
def __call__(self, *args, **kwargs) -> _pragma_class:
151+
return self._pragma_class(self.pragma, value=self.function(*args, **kwargs))
152+
153+
154+
@_dataclass
155+
class _ParametrizedPragma(_ParametrizedClassPragma):
156+
""" A class or property parametrized pragma.
157+
Needs to be applied to a parameter to give a pragma.
158+
"""
159+
_pragma_class: _ClassVar[type] = _Pragma
160+
161+
def __invert__(self) -> _Pragma:
162+
return _Pragma(self.pragma, remove=True)
125163

126164

127165
class _Optionalizer(_schema.PropertyModifier):
@@ -190,30 +228,30 @@ def f(cls: type) -> type:
190228

191229
use_for_null = _annotate(null=True)
192230

193-
_Pragma("qltest_skip")
194-
_Pragma("qltest_collapse_hierarchy")
195-
_Pragma("qltest_uncollapse_hierarchy")
196-
qltest.test_with = lambda cls: _annotate(test_with=cls)
231+
qltest.add(_Pragma("skip"))
232+
qltest.add(_ClassPragma("collapse_hierarchy"))
233+
qltest.add(_ClassPragma("uncollapse_hierarchy"))
234+
qltest.test_with = lambda cls: _annotate(test_with=cls) # inheritable
197235

198-
ql.default_doc_name = lambda doc: _annotate(doc_name=doc)
199-
ql.hideable = _annotate(hideable=True)
200-
_Pragma("ql_internal")
236+
ql.add(_ParametrizedClassPragma("default_doc_name", lambda doc: doc))
237+
ql.hideable = _annotate(hideable=True) # inheritable
238+
ql.add(_Pragma("internal"))
201239

202-
_Pragma("cpp_skip")
240+
cpp.add(_Pragma("skip"))
203241

204-
_Pragma("rust_skip_doc_test")
242+
rust.add(_Pragma("skip_doc_test"))
205243

206-
rust.doc_test_signature = lambda signature: _annotate(rust_doc_test_function=signature)
244+
rust.add(_ParametrizedClassPragma("doc_test_signature", lambda signature: signature))
207245

208246

209247
def group(name: str = "") -> _ClassDecorator:
210248
return _annotate(group=name)
211249

212250

213-
synth.from_class = lambda ref: _annotate(synth=_schema.SynthInfo(
214-
from_class=_schema.get_type_name(ref)))
215-
synth.on_arguments = lambda **kwargs: _annotate(
216-
synth=_schema.SynthInfo(on_arguments={k: _schema.get_type_name(t) for k, t in kwargs.items()}))
251+
synth.add(_ParametrizedClassPragma("from_class", lambda ref: _schema.SynthInfo(
252+
from_class=_schema.get_type_name(ref))), key="synth")
253+
synth.add(_ParametrizedClassPragma("on_arguments", lambda **kwargs:
254+
_schema.SynthInfo(on_arguments={k: _schema.get_type_name(t) for k, t in kwargs.items()})), key="synth")
217255

218256

219257
class _PropertyModifierList(_schema.PropertyModifier):
@@ -251,9 +289,9 @@ def decorator(cls: type) -> _PropertyAnnotation:
251289
if cls.__doc__ is not None:
252290
annotated_cls.__doc__ = cls.__doc__
253291
old_pragmas = getattr(annotated_cls, "_pragmas", None)
254-
new_pragmas = getattr(cls, "_pragmas", [])
292+
new_pragmas = getattr(cls, "_pragmas", {})
255293
if old_pragmas:
256-
old_pragmas.extend(new_pragmas)
294+
old_pragmas.update(new_pragmas)
257295
else:
258296
annotated_cls._pragmas = new_pragmas
259297
for a, v in cls.__dict__.items():

misc/codegen/loaders/schemaloader.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,16 +49,12 @@ def _get_class(cls: type) -> schema.Class:
4949
hideable=getattr(cls, "_hideable", False),
5050
test_with=_get_name(getattr(cls, "_test_with", None)),
5151
# in the following we don't use `getattr` to avoid inheriting
52-
pragmas=cls.__dict__.get("_pragmas", []),
53-
synth=cls.__dict__.get("_synth", None),
52+
pragmas=cls.__dict__.get("_pragmas", {}),
5453
properties=[
5554
a | _PropertyNamer(n)
5655
for n, a in cls.__dict__.get("__annotations__", {}).items()
5756
],
5857
doc=schema.split_doc(cls.__doc__),
59-
default_doc_name=cls.__dict__.get("_doc_name"),
60-
rust_doc_test_function=cls.__dict__.get("_rust_doc_test_function",
61-
schema.Class.rust_doc_test_function)
6258
)
6359

6460

@@ -103,8 +99,8 @@ def fill_is_synth(name: str):
10399
fill_is_synth(root)
104100

105101
for name, cls in classes.items():
106-
if cls.synth is None and is_synth[name]:
107-
cls.synth = True
102+
if is_synth[name]:
103+
cls.mark_synth()
108104

109105

110106
def _fill_hideable_information(classes: typing.Dict[str, schema.Class]):

misc/codegen/test/test_cppgen.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -185,15 +185,15 @@ def test_synth_classes_ignored(generate):
185185
assert generate([
186186
schema.Class(
187187
name="W",
188-
synth=schema.SynthInfo(),
188+
pragmas={"synth": schema.SynthInfo()},
189189
),
190190
schema.Class(
191191
name="X",
192-
synth=schema.SynthInfo(from_class="A"),
192+
pragmas={"synth": schema.SynthInfo(from_class="A")},
193193
),
194194
schema.Class(
195195
name="Y",
196-
synth=schema.SynthInfo(on_arguments={"a": "A", "b": "int"}),
196+
pragmas={"synth": schema.SynthInfo(on_arguments={"a": "A", "b": "int"})},
197197
),
198198
schema.Class(
199199
name="Z",

misc/codegen/test/test_dbschemegen.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -536,9 +536,9 @@ def test_null_class(generate):
536536

537537
def test_synth_classes_ignored(generate):
538538
assert generate([
539-
schema.Class(name="A", synth=schema.SynthInfo()),
540-
schema.Class(name="B", synth=schema.SynthInfo(from_class="A")),
541-
schema.Class(name="C", synth=schema.SynthInfo(on_arguments={"x": "A"})),
539+
schema.Class(name="A", pragmas={"synth": schema.SynthInfo()}),
540+
schema.Class(name="B", pragmas={"synth": schema.SynthInfo(from_class="A")}),
541+
schema.Class(name="C", pragmas={"synth": schema.SynthInfo(on_arguments={"x": "A"})}),
542542
]) == dbscheme.Scheme(
543543
src=schema_file.name,
544544
includes=[],
@@ -549,7 +549,7 @@ def test_synth_classes_ignored(generate):
549549
def test_synth_derived_classes_ignored(generate):
550550
assert generate([
551551
schema.Class(name="A", derived={"B", "C"}),
552-
schema.Class(name="B", bases=["A"], synth=schema.SynthInfo()),
552+
schema.Class(name="B", bases=["A"], pragmas={"synth": schema.SynthInfo()}),
553553
schema.Class(name="C", bases=["A"]),
554554
]) == dbscheme.Scheme(
555555
src=schema_file.name,

misc/codegen/test/test_qlgen.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -922,7 +922,7 @@ def test_property_on_class_with_default_doc_name(generate_classes):
922922
assert generate_classes([
923923
schema.Class("MyObject", properties=[
924924
schema.SingleProperty("foo", "bar")],
925-
default_doc_name="baz"),
925+
pragmas={"ql_default_doc_name": "baz"}),
926926
]) == {
927927
"MyObject.qll": (a_ql_class_public(name="MyObject"),
928928
a_ql_stub(name="MyObject"),
@@ -937,7 +937,7 @@ def test_property_on_class_with_default_doc_name(generate_classes):
937937

938938
def test_stub_on_class_with_synth_from_class(generate_classes):
939939
assert generate_classes([
940-
schema.Class("MyObject", synth=schema.SynthInfo(from_class="A"),
940+
schema.Class("MyObject", pragmas={"synth": schema.SynthInfo(from_class="A")},
941941
properties=[schema.SingleProperty("foo", "bar")]),
942942
]) == {
943943
"MyObject.qll": (a_ql_class_public(name="MyObject"), a_ql_stub(name="MyObject", synth_accessors=[
@@ -952,7 +952,7 @@ def test_stub_on_class_with_synth_from_class(generate_classes):
952952

953953
def test_stub_on_class_with_synth_on_arguments(generate_classes):
954954
assert generate_classes([
955-
schema.Class("MyObject", synth=schema.SynthInfo(on_arguments={"base": "A", "index": "int", "label": "string"}),
955+
schema.Class("MyObject", pragmas={"synth": schema.SynthInfo(on_arguments={"base": "A", "index": "int", "label": "string"})},
956956
properties=[schema.SingleProperty("foo", "bar")]),
957957
]) == {
958958
"MyObject.qll": (a_ql_class_public(name="MyObject"), a_ql_stub(name="MyObject", synth_accessors=[

0 commit comments

Comments
 (0)