Skip to content

Commit 594045b

Browse files
authored
Merge pull request #17530 from github/redsun82/codegen-annotate
Codegen: allow full annotation of classes
2 parents f2e943f + 74c0fa7 commit 594045b

File tree

6 files changed

+219
-53
lines changed

6 files changed

+219
-53
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: 101 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,65 @@
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+
24+
class _DocModifierMetaclass(type(_schema.PropertyModifier)):
25+
# make ~doc same as doc(None)
26+
def __invert__(self) -> _schema.PropertyModifier:
27+
return _DocModifier(None)
1428

1529

1630
@_dataclass
17-
class _DocModifier(_schema.PropertyModifier):
18-
doc: str
31+
class _DocModifier(_schema.PropertyModifier, metaclass=_DocModifierMetaclass):
32+
doc: str | None
1933

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

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

2649
@_dataclass
27-
class _DescModifier(_schema.PropertyModifier):
28-
description: str
50+
class _DescModifier(_schema.PropertyModifier, metaclass=_DescModifierMetaclass):
51+
description: str | None
2952

3053
def modify(self, prop: _schema.Property):
3154
prop.description = _schema.split_doc(self.description)
3255

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

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

3964

4065
class _Namespace:
@@ -44,9 +69,15 @@ def __init__(self, **kwargs):
4469
self.__dict__.update(kwargs)
4570

4671

72+
@_dataclass
4773
class _SynthModifier(_schema.PropertyModifier, _Namespace):
74+
synth: bool = True
75+
4876
def modify(self, prop: _schema.Property):
49-
prop.synth = True
77+
prop.synth = self.synth
78+
79+
def negate(self) -> "PropertyModifier":
80+
return _SynthModifier(False)
5081

5182

5283
qltest = _Namespace()
@@ -63,22 +94,35 @@ class _Pragma(_schema.PropertyModifier):
6394
For schema classes it acts as a python decorator with `@`.
6495
"""
6596
pragma: str
97+
remove: bool = False
6698

6799
def __post_init__(self):
68100
namespace, _, name = self.pragma.partition('_')
69101
setattr(globals()[namespace], name, self)
70102

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

74109
def __call__(self, cls: type) -> type:
75110
""" use this pragma as a decorator on classes """
76111
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:
112+
self._apply(cls._pragmas)
113+
elif not self.remove:
79114
cls._pragmas = [self.pragma]
80115
return cls
81116

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

83127
class _Optionalizer(_schema.PropertyModifier):
84128
def modify(self, prop: _schema.Property):
@@ -172,17 +216,57 @@ def group(name: str = "") -> _ClassDecorator:
172216
synth=_schema.SynthInfo(on_arguments={k: _schema.get_type_name(t) for k, t in kwargs.items()}))
173217

174218

175-
def annotate(annotated_cls: type) -> _Callable[[type], None]:
219+
class _PropertyModifierList(_schema.PropertyModifier):
220+
def __init__(self):
221+
self._mods = []
222+
223+
def __or__(self, other: _schema.PropertyModifier):
224+
self._mods.append(other)
225+
return self
226+
227+
def modify(self, prop: Property):
228+
for m in self._mods:
229+
m.modify(prop)
230+
231+
232+
class _PropertyAnnotation:
233+
def __or__(self, other: _schema.PropertyModifier):
234+
return _PropertyModifierList() | other
235+
236+
237+
_ = _PropertyAnnotation()
238+
239+
240+
def annotate(annotated_cls: type) -> _Callable[[type], _PropertyAnnotation]:
176241
"""
177242
Add or modify schema annotations after a class has been defined
178243
For the moment, only docstring annotation is supported. In the future, any kind of
179244
modification will be allowed.
180245
181246
The name of the class used for annotation must be `_`
182247
"""
183-
def decorator(cls: type) -> None:
248+
def decorator(cls: type) -> _PropertyAnnotation:
184249
if cls.__name__ != "_":
185250
raise _schema.Error("Annotation classes must be named _")
186-
annotated_cls.__doc__ = cls.__doc__
187-
return None
251+
if cls.__doc__ is not None:
252+
annotated_cls.__doc__ = cls.__doc__
253+
old_pragmas = getattr(annotated_cls, "_pragmas", None)
254+
new_pragmas = getattr(cls, "_pragmas", [])
255+
if old_pragmas:
256+
old_pragmas.extend(new_pragmas)
257+
else:
258+
annotated_cls._pragmas = new_pragmas
259+
for a, v in cls.__dict__.items():
260+
# transfer annotations
261+
if a.startswith("_") and not a.startswith("__") and a != "_pragmas":
262+
setattr(annotated_cls, a, v)
263+
for p, a in cls.__annotations__.items():
264+
if p in annotated_cls.__annotations__:
265+
annotated_cls.__annotations__[p] |= a
266+
elif isinstance(a, (_PropertyAnnotation, _PropertyModifierList)):
267+
raise _schema.Error(f"annotated property {p} not present in annotated class "
268+
f"{annotated_cls.__name__}")
269+
else:
270+
annotated_cls.__annotations__[p] = a
271+
return _
188272
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: 87 additions & 1 deletion
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
@@ -762,18 +763,103 @@ class data:
762763
class Root:
763764
""" old docstring """
764765

766+
class A(Root):
767+
""" A docstring """
768+
765769
@defs.annotate(Root)
766770
class _:
767771
"""
768772
new
769773
docstring
770774
"""
771775

776+
@defs.annotate(A)
777+
class _:
778+
pass
779+
780+
assert data.classes == {
781+
"Root": schema.Class("Root", doc=["new", "docstring"], derived={"A"}),
782+
"A": schema.Class("A", bases=["Root"], doc=["A docstring"]),
783+
}
784+
785+
786+
def test_annotate_decorations():
787+
@load
788+
class data:
789+
@defs.qltest.skip
790+
class Root:
791+
pass
792+
793+
@defs.annotate(Root)
794+
@defs.qltest.collapse_hierarchy
795+
@defs.ql.hideable
796+
@defs.cpp.skip
797+
class _:
798+
pass
799+
800+
assert data.classes == {
801+
"Root": schema.Class("Root", hideable=True,
802+
pragmas=["qltest_skip", "cpp_skip", "qltest_collapse_hierarchy"]),
803+
}
804+
805+
806+
def test_annotate_fields():
807+
@load
808+
class data:
809+
class Root:
810+
x: defs.int
811+
y: defs.optional["Root"] | defs.child
812+
813+
@defs.annotate(Root)
814+
class _:
815+
x: defs._ | defs.doc("foo")
816+
y: defs._ | defs.ql.internal
817+
z: defs.string
818+
819+
assert data.classes == {
820+
"Root": schema.Class("Root", properties=[
821+
schema.SingleProperty("x", "int", doc="foo"),
822+
schema.OptionalProperty("y", "Root", pragmas=["ql_internal"], is_child=True),
823+
schema.SingleProperty("z", "string"),
824+
]),
825+
}
826+
827+
828+
def test_annotate_fields_negations():
829+
@load
830+
class data:
831+
class Root:
832+
x: defs.int | defs.ql.internal | defs.qltest.skip
833+
y: defs.optional["Root"] | defs.child | defs.desc("foo\nbar\n")
834+
z: defs.string | defs.synth | defs.doc("foo")
835+
836+
@defs.annotate(Root)
837+
class _:
838+
x: defs._ | ~defs.ql.internal
839+
y: defs._ | ~defs.child | ~defs.ql.internal | ~defs.desc
840+
z: defs._ | ~defs.synth | ~defs.doc
841+
772842
assert data.classes == {
773-
"Root": schema.Class("Root", doc=["new", "docstring"]),
843+
"Root": schema.Class("Root", properties=[
844+
schema.SingleProperty("x", "int", pragmas=["qltest_skip"]),
845+
schema.OptionalProperty("y", "Root"),
846+
schema.SingleProperty("z", "string"),
847+
]),
774848
}
775849

776850

851+
def test_annotate_non_existing_field():
852+
with pytest.raises(schema.Error):
853+
@load
854+
class data:
855+
class Root:
856+
pass
857+
858+
@defs.annotate(Root)
859+
class _:
860+
x: defs._ | defs.doc("foo")
861+
862+
777863
def test_annotate_not_underscore():
778864
with pytest.raises(schema.Error):
779865
@load

0 commit comments

Comments
 (0)