Skip to content

Commit cc5882a

Browse files
author
Paolo Tranquilli
committed
Codegen: allow full annotation of classes
1 parent cf5d56a commit cc5882a

File tree

6 files changed

+179
-51
lines changed

6 files changed

+179
-51
lines changed

misc/codegen/lib/schema.py

Lines changed: 27 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
""" schema format representation """
2+
import abc
23
import typing
34
from dataclasses import dataclass, field
45
from typing import List, Set, Union, Dict, Optional
@@ -136,40 +137,29 @@ def null_class(self):
136137
TypeRef = Union[type, str]
137138

138139

139-
@functools.singledispatch
140140
def get_type_name(arg: TypeRef) -> str:
141-
raise Error(f"Not a schema type or string ({arg})")
141+
match arg:
142+
case type():
143+
return arg.__name__
144+
case str():
145+
return arg
146+
case _:
147+
raise Error(f"Not a schema type or string ({arg})")
142148

143149

144-
@get_type_name.register
145-
def _(arg: type):
146-
return arg.__name__
147-
148-
149-
@get_type_name.register
150-
def _(arg: str):
151-
return arg
152-
153-
154-
@functools.singledispatch
155150
def _make_property(arg: object) -> Property:
156-
if arg is predicate_marker:
157-
return PredicateProperty()
158-
raise Error(f"Illegal property specifier {arg}")
159-
160-
161-
@_make_property.register(str)
162-
@_make_property.register(type)
163-
def _(arg: TypeRef):
164-
return SingleProperty(type=get_type_name(arg))
165-
166-
167-
@_make_property.register
168-
def _(arg: Property):
169-
return arg
170-
171-
172-
class PropertyModifier:
151+
match arg:
152+
case _ if arg is predicate_marker:
153+
return PredicateProperty()
154+
case str() | type():
155+
return SingleProperty(type=get_type_name(arg))
156+
case Property():
157+
return arg
158+
case _:
159+
raise Error(f"Illegal property specifier {arg}")
160+
161+
162+
class PropertyModifier(abc.ABC):
173163
""" Modifier of `Property` objects.
174164
Being on the right of `|` it will trigger construction of a `Property` from
175165
the left operand.
@@ -180,8 +170,14 @@ def __ror__(self, other: object) -> Property:
180170
self.modify(ret)
181171
return ret
182172

173+
def __invert__(self) -> "PropertyModifier":
174+
return self.negate()
175+
183176
def modify(self, prop: Property):
184-
raise NotImplementedError
177+
...
178+
179+
def negate(self) -> "PropertyModifier":
180+
...
185181

186182

187183
def split_doc(doc):

misc/codegen/lib/schemadefs.py

Lines changed: 90 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,66 @@
1-
from typing import Callable as _Callable
1+
from typing import Callable as _Callable, List as _List
22
from misc.codegen.lib import schema as _schema
33
import inspect as _inspect
44
from dataclasses import dataclass as _dataclass
55

6+
from misc.codegen.lib.schema import Property
67

8+
9+
@_dataclass
710
class _ChildModifier(_schema.PropertyModifier):
11+
child: bool = True
12+
813
def modify(self, prop: _schema.Property):
914
if prop.type is None or prop.type[0].islower():
1015
raise _schema.Error("Non-class properties cannot be children")
1116
if prop.is_unordered:
1217
raise _schema.Error("Set properties cannot be children")
13-
prop.is_child = True
18+
prop.is_child = self.child
19+
20+
def negate(self) -> _schema.PropertyModifier:
21+
return _ChildModifier(False)
22+
23+
# make ~doc same as doc(None)
24+
25+
26+
class _DocModifierMetaclass(type(_schema.PropertyModifier)):
27+
def __invert__(self) -> _schema.PropertyModifier:
28+
return _DocModifier(None)
1429

1530

1631
@_dataclass
17-
class _DocModifier(_schema.PropertyModifier):
18-
doc: str
32+
class _DocModifier(_schema.PropertyModifier, metaclass=_DocModifierMetaclass):
33+
doc: str | None
1934

2035
def modify(self, prop: _schema.Property):
21-
if "\n" in self.doc or self.doc[-1] == ".":
36+
if self.doc and ("\n" in self.doc or self.doc[-1] == "."):
2237
raise _schema.Error("No newlines or trailing dots are allowed in doc, did you intend to use desc?")
2338
prop.doc = self.doc
2439

40+
def negate(self) -> _schema.PropertyModifier:
41+
return _DocModifier(None)
42+
43+
44+
# make ~desc same as desc(None)
45+
class _DescModifierMetaclass(type(_schema.PropertyModifier)):
46+
def __invert__(self) -> _schema.PropertyModifier:
47+
return _DescModifier(None)
48+
2549

2650
@_dataclass
27-
class _DescModifier(_schema.PropertyModifier):
28-
description: str
51+
class _DescModifier(_schema.PropertyModifier, metaclass=_DescModifierMetaclass):
52+
description: str | None
2953

3054
def modify(self, prop: _schema.Property):
3155
prop.description = _schema.split_doc(self.description)
3256

57+
def negate(self) -> _schema.PropertyModifier:
58+
return _DescModifier(None)
59+
3360

3461
def include(source: str):
3562
# add to `includes` variable in calling context
36-
_inspect.currentframe().f_back.f_locals.setdefault(
37-
"__includes", []).append(source)
63+
_inspect.currentframe().f_back.f_locals.setdefault("includes", []).append(source)
3864

3965

4066
class _Namespace:
@@ -44,9 +70,15 @@ def __init__(self, **kwargs):
4470
self.__dict__.update(kwargs)
4571

4672

73+
@_dataclass
4774
class _SynthModifier(_schema.PropertyModifier, _Namespace):
75+
synth: bool = True
76+
4877
def modify(self, prop: _schema.Property):
49-
prop.synth = True
78+
prop.synth = self.synth
79+
80+
def negate(self) -> "PropertyModifier":
81+
return _SynthModifier(False)
5082

5183

5284
qltest = _Namespace()
@@ -63,22 +95,35 @@ class _Pragma(_schema.PropertyModifier):
6395
For schema classes it acts as a python decorator with `@`.
6496
"""
6597
pragma: str
98+
remove: bool = False
6699

67100
def __post_init__(self):
68101
namespace, _, name = self.pragma.partition('_')
69102
setattr(globals()[namespace], name, self)
70103

71104
def modify(self, prop: _schema.Property):
72-
prop.pragmas.append(self.pragma)
105+
self._apply(prop.pragmas)
106+
107+
def negate(self) -> "PropertyModifier":
108+
return _Pragma(self.pragma, remove=True)
73109

74110
def __call__(self, cls: type) -> type:
75111
""" use this pragma as a decorator on classes """
76112
if "_pragmas" in cls.__dict__: # not using hasattr as we don't want to land on inherited pragmas
77-
cls._pragmas.append(self.pragma)
78-
else:
113+
self._apply(cls._pragmas)
114+
elif not self.remove:
79115
cls._pragmas = [self.pragma]
80116
return cls
81117

118+
def _apply(self, pragmas: _List[str]) -> None:
119+
if self.remove:
120+
try:
121+
pragmas.remove(self.pragma)
122+
except ValueError:
123+
pass
124+
else:
125+
pragmas.append(self.pragma)
126+
82127

83128
class _Optionalizer(_schema.PropertyModifier):
84129
def modify(self, prop: _schema.Property):
@@ -172,17 +217,46 @@ def group(name: str = "") -> _ClassDecorator:
172217
synth=_schema.SynthInfo(on_arguments={k: _schema.get_type_name(t) for k, t in kwargs.items()}))
173218

174219

175-
def annotate(annotated_cls: type) -> _Callable[[type], None]:
220+
class _PropertyModifierList(_schema.PropertyModifier):
221+
def __init__(self):
222+
self._mods = []
223+
224+
def __or__(self, other: _schema.PropertyModifier):
225+
self._mods.append(other)
226+
return self
227+
228+
def modify(self, prop: Property):
229+
for m in self._mods:
230+
m.modify(prop)
231+
232+
233+
class _PropertyAnnotation:
234+
def __or__(self, other: _schema.PropertyModifier):
235+
return _PropertyModifierList() | other
236+
237+
238+
_ = _PropertyAnnotation()
239+
240+
241+
def annotate(annotated_cls: type) -> _Callable[[type], _PropertyAnnotation]:
176242
"""
177243
Add or modify schema annotations after a class has been defined
178244
For the moment, only docstring annotation is supported. In the future, any kind of
179245
modification will be allowed.
180246
181247
The name of the class used for annotation must be `_`
182248
"""
183-
def decorator(cls: type) -> None:
249+
def decorator(cls: type) -> _PropertyAnnotation:
184250
if cls.__name__ != "_":
185251
raise _schema.Error("Annotation classes must be named _")
186252
annotated_cls.__doc__ = cls.__doc__
187-
return None
253+
for p, a in cls.__annotations__.items():
254+
if p in annotated_cls.__annotations__:
255+
annotated_cls.__annotations__[p] |= a
256+
elif isinstance(a, (_PropertyAnnotation, _PropertyModifierList)):
257+
raise _schema.Error(f"annotated property {p} not present in annotated class "
258+
f"{annotated_cls.__name__}")
259+
else:
260+
annotated_cls.__annotations__[p] = a
261+
return _
188262
return decorator

misc/codegen/loaders/schemaloader.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ def load(m: types.ModuleType) -> schema.Schema:
136136
for name, data in m.__dict__.items():
137137
if hasattr(defs, name):
138138
continue
139-
if name == "__includes":
139+
if name == "includes":
140140
includes = data
141141
continue
142142
if name.startswith("__") or name == "_":

misc/codegen/test/test_schemaloader.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import sys
22

33
import pytest
4+
from misc.codegen.lib.schemadefs import optional
45

56
from misc.codegen.test.utils import *
67
from misc.codegen.lib import schemadefs as defs
@@ -774,6 +775,63 @@ class _:
774775
}
775776

776777

778+
def test_annotate_fields():
779+
@load
780+
class data:
781+
class Root:
782+
x: defs.int
783+
y: defs.optional["Root"] | defs.child
784+
785+
@defs.annotate(Root)
786+
class _:
787+
x: defs._ | defs.doc("foo")
788+
y: defs._ | defs.ql.internal
789+
z: defs.string
790+
791+
assert data.classes == {
792+
"Root": schema.Class("Root", properties=[
793+
schema.SingleProperty("x", "int", doc="foo"),
794+
schema.OptionalProperty("y", "Root", pragmas=["ql_internal"], is_child=True),
795+
schema.SingleProperty("z", "string"),
796+
]),
797+
}
798+
799+
800+
def test_annotate_fields_negations():
801+
@load
802+
class data:
803+
class Root:
804+
x: defs.int | defs.ql.internal | defs.qltest.skip
805+
y: defs.optional["Root"] | defs.child | defs.desc("foo\nbar\n")
806+
z: defs.string | defs.synth | defs.doc("foo")
807+
808+
@defs.annotate(Root)
809+
class _:
810+
x: defs._ | ~defs.ql.internal
811+
y: defs._ | ~defs.child | ~defs.ql.internal | ~defs.desc
812+
z: defs._ | ~defs.synth | ~defs.doc
813+
814+
assert data.classes == {
815+
"Root": schema.Class("Root", properties=[
816+
schema.SingleProperty("x", "int", pragmas=["qltest_skip"]),
817+
schema.OptionalProperty("y", "Root"),
818+
schema.SingleProperty("z", "string"),
819+
]),
820+
}
821+
822+
823+
def test_annotate_non_existing_field():
824+
with pytest.raises(schema.Error):
825+
@load
826+
class data:
827+
class Root:
828+
pass
829+
830+
@defs.annotate(Root)
831+
class _:
832+
x: defs._ | defs.doc("foo")
833+
834+
777835
def test_annotate_not_underscore():
778836
with pytest.raises(schema.Error):
779837
@load

rust/schema/__init__.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,3 @@
1111

1212
from .prelude import *
1313
from .ast import *
14-
15-
include("../shared/tree-sitter-extractor/src/generator/prefix.dbscheme")
16-
include("prefix.dbscheme")

rust/schema/prelude.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
from misc.codegen.lib.schemadefs import *
22

3+
include("../shared/tree-sitter-extractor/src/generator/prefix.dbscheme")
4+
include("prefix.dbscheme")
5+
36
@qltest.skip
47
class Element:
58
pass

0 commit comments

Comments
 (0)