diff --git a/misc/codegen/generators/qlgen.py b/misc/codegen/generators/qlgen.py index 952889ee9c2a..3240f28eecd9 100755 --- a/misc/codegen/generators/qlgen.py +++ b/misc/codegen/generators/qlgen.py @@ -96,7 +96,7 @@ def _get_doc(cls: schema.Class, prop: schema.Property, plural=None): return format.format(**{noun: transform(noun) for noun in nouns}) prop_name = _humanize(prop.name) - class_name = cls.default_doc_name or _humanize(inflection.underscore(cls.name)) + class_name = cls.pragmas.get("ql_default_doc_name", _humanize(inflection.underscore(cls.name))) if prop.is_predicate: return f"this {class_name} {prop_name}" if plural is not None: diff --git a/misc/codegen/generators/rusttestgen.py b/misc/codegen/generators/rusttestgen.py index e9fee845c793..a44c5f2b2414 100644 --- a/misc/codegen/generators/rusttestgen.py +++ b/misc/codegen/generators/rusttestgen.py @@ -55,7 +55,7 @@ def generate(opts, renderer): continue assert not adding_code, "Unterminated code block in docstring: " + "\n".join(cls.doc) test_name = inflection.underscore(cls.name) - signature = cls.rust_doc_test_function + signature = cls.pragmas.get("rust_doc_test_signature", "() -> ()") fn = signature and Function(f"test_{test_name}", signature) if fn: indent = 4 * " " diff --git a/misc/codegen/lib/schema.py b/misc/codegen/lib/schema.py index b950298fd973..bdaaba32c20b 100644 --- a/misc/codegen/lib/schema.py +++ b/misc/codegen/lib/schema.py @@ -32,11 +32,15 @@ class Kind(Enum): name: Optional[str] = None type: Optional[str] = None is_child: bool = False - pragmas: List[str] = field(default_factory=list) + pragmas: List[str] | Dict[str, object] = field(default_factory=dict) doc: Optional[str] = None description: List[str] = field(default_factory=list) synth: bool = False + def __post_init__(self): + if not isinstance(self.pragmas, dict): + self.pragmas = dict.fromkeys(self.pragmas, None) + @property def is_single(self) -> bool: return self.kind == self.Kind.SINGLE @@ -88,14 +92,14 @@ class Class: derived: Set[str] = field(default_factory=set) properties: List[Property] = field(default_factory=list) group: str = "" - pragmas: List[str] = field(default_factory=list) - synth: Optional[Union[SynthInfo, bool]] = None - """^^^ filled with `True` for non-final classes with only synthesized final descendants """ + pragmas: List[str] | Dict[str, object] = field(default_factory=dict) doc: List[str] = field(default_factory=list) - default_doc_name: Optional[str] = None hideable: bool = False test_with: Optional[str] = None - rust_doc_test_function: Optional["FunctionInfo"] = "() -> ()" # TODO: parametrized pragmas + + def __post_init__(self): + if not isinstance(self.pragmas, dict): + self.pragmas = dict.fromkeys(self.pragmas, None) @property def final(self): @@ -108,13 +112,21 @@ def check_types(self, known: typing.Iterable[str]): _check_type(d, known) for p in self.properties: _check_type(p.type, known) - if self.synth is not None: - _check_type(self.synth.from_class, known) - if self.synth.on_arguments is not None: - for t in self.synth.on_arguments.values(): + if "synth" in self.pragmas: + synth = self.pragmas["synth"] + _check_type(synth.from_class, known) + if synth.on_arguments is not None: + for t in synth.on_arguments.values(): _check_type(t, known) _check_type(self.test_with, known) + @property + def synth(self) -> SynthInfo | bool | None: + return self.pragmas.get("synth") + + def mark_synth(self): + self.pragmas.setdefault("synth", True) + @dataclass class Schema: diff --git a/misc/codegen/lib/schemadefs.py b/misc/codegen/lib/schemadefs.py index adcaf4fbf1aa..151d5ac98435 100644 --- a/misc/codegen/lib/schemadefs.py +++ b/misc/codegen/lib/schemadefs.py @@ -1,4 +1,4 @@ -from typing import Callable as _Callable, List as _List +from typing import Callable as _Callable, Dict as _Dict, ClassVar as _ClassVar from misc.codegen.lib import schema as _schema import inspect as _inspect from dataclasses import dataclass as _dataclass @@ -62,11 +62,14 @@ def include(source: str): _inspect.currentframe().f_back.f_locals.setdefault("includes", []).append(source) +@_dataclass class _Namespace: """ simple namespacing mechanism """ + name: str - def __init__(self, **kwargs): - self.__dict__.update(kwargs) + def add(self, pragma: "_PragmaBase", key: str | None = None): + self.__dict__[pragma.pragma] = pragma + pragma.pragma = key or f"{self.name}_{pragma.pragma}" @_dataclass @@ -77,51 +80,86 @@ def modify(self, prop: _schema.Property): prop.synth = self.synth def negate(self) -> "PropertyModifier": - return _SynthModifier(False) + return _SynthModifier(self.name, False) + + +qltest = _Namespace("qltest") +ql = _Namespace("ql") +cpp = _Namespace("cpp") +rust = _Namespace("rust") +synth = _SynthModifier("synth") + + +@_dataclass +class _PragmaBase: + pragma: str + + +@_dataclass +class _ClassPragma(_PragmaBase): + """ A class pragma. + For schema classes it acts as a python decorator with `@`. + """ + value: object = None + def __call__(self, cls: type) -> type: + """ use this pragma as a decorator on classes """ + # not using hasattr as we don't want to land on inherited pragmas + if "_pragmas" not in cls.__dict__: + cls._pragmas = {} + self._apply(cls._pragmas) + return cls -qltest = _Namespace() -ql = _Namespace() -cpp = _Namespace() -rust = _Namespace() -synth = _SynthModifier() + def _apply(self, pragmas: _Dict[str, object]) -> None: + pragmas[self.pragma] = self.value @_dataclass -class _Pragma(_schema.PropertyModifier): +class _Pragma(_ClassPragma, _schema.PropertyModifier): """ A class or property pragma. For properties, it functions similarly to a `_PropertyModifier` with `|`, adding the pragma. For schema classes it acts as a python decorator with `@`. """ - pragma: str remove: bool = False - def __post_init__(self): - namespace, _, name = self.pragma.partition('_') - setattr(globals()[namespace], name, self) - def modify(self, prop: _schema.Property): self._apply(prop.pragmas) def negate(self) -> "PropertyModifier": return _Pragma(self.pragma, remove=True) - def __call__(self, cls: type) -> type: - """ use this pragma as a decorator on classes """ - if "_pragmas" in cls.__dict__: # not using hasattr as we don't want to land on inherited pragmas - self._apply(cls._pragmas) - elif not self.remove: - cls._pragmas = [self.pragma] - return cls - - def _apply(self, pragmas: _List[str]) -> None: + def _apply(self, pragmas: _Dict[str, object]) -> None: if self.remove: - try: - pragmas.remove(self.pragma) - except ValueError: - pass + pragmas.pop(self.pragma, None) else: - pragmas.append(self.pragma) + super()._apply(pragmas) + + +@_dataclass +class _ParametrizedClassPragma(_PragmaBase): + """ A class parametrized pragma. + Needs to be applied to a parameter to give a class pragma. + """ + _pragma_class: _ClassVar[type] = _ClassPragma + + function: _Callable[..., object] = None + + def __post_init__(self): + self.__signature__ = _inspect.signature(self.function).replace(return_annotation=self._pragma_class) + + def __call__(self, *args, **kwargs) -> _pragma_class: + return self._pragma_class(self.pragma, value=self.function(*args, **kwargs)) + + +@_dataclass +class _ParametrizedPragma(_ParametrizedClassPragma): + """ A class or property parametrized pragma. + Needs to be applied to a parameter to give a pragma. + """ + _pragma_class: _ClassVar[type] = _Pragma + + def __invert__(self) -> _Pragma: + return _Pragma(self.pragma, remove=True) class _Optionalizer(_schema.PropertyModifier): @@ -190,30 +228,30 @@ def f(cls: type) -> type: use_for_null = _annotate(null=True) -_Pragma("qltest_skip") -_Pragma("qltest_collapse_hierarchy") -_Pragma("qltest_uncollapse_hierarchy") -qltest.test_with = lambda cls: _annotate(test_with=cls) +qltest.add(_Pragma("skip")) +qltest.add(_ClassPragma("collapse_hierarchy")) +qltest.add(_ClassPragma("uncollapse_hierarchy")) +qltest.test_with = lambda cls: _annotate(test_with=cls) # inheritable -ql.default_doc_name = lambda doc: _annotate(doc_name=doc) -ql.hideable = _annotate(hideable=True) -_Pragma("ql_internal") +ql.add(_ParametrizedClassPragma("default_doc_name", lambda doc: doc)) +ql.hideable = _annotate(hideable=True) # inheritable +ql.add(_Pragma("internal")) -_Pragma("cpp_skip") +cpp.add(_Pragma("skip")) -_Pragma("rust_skip_doc_test") +rust.add(_Pragma("skip_doc_test")) -rust.doc_test_signature = lambda signature: _annotate(rust_doc_test_function=signature) +rust.add(_ParametrizedClassPragma("doc_test_signature", lambda signature: signature)) def group(name: str = "") -> _ClassDecorator: return _annotate(group=name) -synth.from_class = lambda ref: _annotate(synth=_schema.SynthInfo( - from_class=_schema.get_type_name(ref))) -synth.on_arguments = lambda **kwargs: _annotate( - synth=_schema.SynthInfo(on_arguments={k: _schema.get_type_name(t) for k, t in kwargs.items()})) +synth.add(_ParametrizedClassPragma("from_class", lambda ref: _schema.SynthInfo( + from_class=_schema.get_type_name(ref))), key="synth") +synth.add(_ParametrizedClassPragma("on_arguments", lambda **kwargs: + _schema.SynthInfo(on_arguments={k: _schema.get_type_name(t) for k, t in kwargs.items()})), key="synth") class _PropertyModifierList(_schema.PropertyModifier): @@ -251,9 +289,9 @@ def decorator(cls: type) -> _PropertyAnnotation: if cls.__doc__ is not None: annotated_cls.__doc__ = cls.__doc__ old_pragmas = getattr(annotated_cls, "_pragmas", None) - new_pragmas = getattr(cls, "_pragmas", []) + new_pragmas = getattr(cls, "_pragmas", {}) if old_pragmas: - old_pragmas.extend(new_pragmas) + old_pragmas.update(new_pragmas) else: annotated_cls._pragmas = new_pragmas for a, v in cls.__dict__.items(): diff --git a/misc/codegen/loaders/schemaloader.py b/misc/codegen/loaders/schemaloader.py index d00eb2b7ce72..822a4d696d16 100644 --- a/misc/codegen/loaders/schemaloader.py +++ b/misc/codegen/loaders/schemaloader.py @@ -49,16 +49,12 @@ def _get_class(cls: type) -> schema.Class: hideable=getattr(cls, "_hideable", False), test_with=_get_name(getattr(cls, "_test_with", None)), # in the following we don't use `getattr` to avoid inheriting - pragmas=cls.__dict__.get("_pragmas", []), - synth=cls.__dict__.get("_synth", None), + pragmas=cls.__dict__.get("_pragmas", {}), properties=[ a | _PropertyNamer(n) for n, a in cls.__dict__.get("__annotations__", {}).items() ], doc=schema.split_doc(cls.__doc__), - default_doc_name=cls.__dict__.get("_doc_name"), - rust_doc_test_function=cls.__dict__.get("_rust_doc_test_function", - schema.Class.rust_doc_test_function) ) @@ -103,8 +99,8 @@ def fill_is_synth(name: str): fill_is_synth(root) for name, cls in classes.items(): - if cls.synth is None and is_synth[name]: - cls.synth = True + if is_synth[name]: + cls.mark_synth() def _fill_hideable_information(classes: typing.Dict[str, schema.Class]): diff --git a/misc/codegen/test/test_cppgen.py b/misc/codegen/test/test_cppgen.py index fcd9b15e8d02..c8e05fa1d127 100644 --- a/misc/codegen/test/test_cppgen.py +++ b/misc/codegen/test/test_cppgen.py @@ -185,15 +185,15 @@ def test_synth_classes_ignored(generate): assert generate([ schema.Class( name="W", - synth=schema.SynthInfo(), + pragmas={"synth": schema.SynthInfo()}, ), schema.Class( name="X", - synth=schema.SynthInfo(from_class="A"), + pragmas={"synth": schema.SynthInfo(from_class="A")}, ), schema.Class( name="Y", - synth=schema.SynthInfo(on_arguments={"a": "A", "b": "int"}), + pragmas={"synth": schema.SynthInfo(on_arguments={"a": "A", "b": "int"})}, ), schema.Class( name="Z", diff --git a/misc/codegen/test/test_dbschemegen.py b/misc/codegen/test/test_dbschemegen.py index 1fbbc0467855..50ad70ed2b82 100644 --- a/misc/codegen/test/test_dbschemegen.py +++ b/misc/codegen/test/test_dbschemegen.py @@ -536,9 +536,9 @@ def test_null_class(generate): def test_synth_classes_ignored(generate): assert generate([ - schema.Class(name="A", synth=schema.SynthInfo()), - schema.Class(name="B", synth=schema.SynthInfo(from_class="A")), - schema.Class(name="C", synth=schema.SynthInfo(on_arguments={"x": "A"})), + schema.Class(name="A", pragmas={"synth": schema.SynthInfo()}), + schema.Class(name="B", pragmas={"synth": schema.SynthInfo(from_class="A")}), + schema.Class(name="C", pragmas={"synth": schema.SynthInfo(on_arguments={"x": "A"})}), ]) == dbscheme.Scheme( src=schema_file.name, includes=[], @@ -549,7 +549,7 @@ def test_synth_classes_ignored(generate): def test_synth_derived_classes_ignored(generate): assert generate([ schema.Class(name="A", derived={"B", "C"}), - schema.Class(name="B", bases=["A"], synth=schema.SynthInfo()), + schema.Class(name="B", bases=["A"], pragmas={"synth": schema.SynthInfo()}), schema.Class(name="C", bases=["A"]), ]) == dbscheme.Scheme( src=schema_file.name, diff --git a/misc/codegen/test/test_qlgen.py b/misc/codegen/test/test_qlgen.py index f364fcbd92c3..8072a96f2771 100644 --- a/misc/codegen/test/test_qlgen.py +++ b/misc/codegen/test/test_qlgen.py @@ -922,7 +922,7 @@ def test_property_on_class_with_default_doc_name(generate_classes): assert generate_classes([ schema.Class("MyObject", properties=[ schema.SingleProperty("foo", "bar")], - default_doc_name="baz"), + pragmas={"ql_default_doc_name": "baz"}), ]) == { "MyObject.qll": (a_ql_class_public(name="MyObject"), a_ql_stub(name="MyObject"), @@ -937,7 +937,7 @@ def test_property_on_class_with_default_doc_name(generate_classes): def test_stub_on_class_with_synth_from_class(generate_classes): assert generate_classes([ - schema.Class("MyObject", synth=schema.SynthInfo(from_class="A"), + schema.Class("MyObject", pragmas={"synth": schema.SynthInfo(from_class="A")}, properties=[schema.SingleProperty("foo", "bar")]), ]) == { "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): def test_stub_on_class_with_synth_on_arguments(generate_classes): assert generate_classes([ - schema.Class("MyObject", synth=schema.SynthInfo(on_arguments={"base": "A", "index": "int", "label": "string"}), + schema.Class("MyObject", pragmas={"synth": schema.SynthInfo(on_arguments={"base": "A", "index": "int", "label": "string"})}, properties=[schema.SingleProperty("foo", "bar")]), ]) == { "MyObject.qll": (a_ql_class_public(name="MyObject"), a_ql_stub(name="MyObject", synth_accessors=[ diff --git a/misc/codegen/test/test_schemaloader.py b/misc/codegen/test/test_schemaloader.py index a9b457b83181..1a96e102a14b 100644 --- a/misc/codegen/test/test_schemaloader.py +++ b/misc/codegen/test/test_schemaloader.py @@ -269,15 +269,21 @@ class A: x: spec | defs.child -_pragmas = [(defs.qltest.skip, "qltest_skip"), - (defs.qltest.collapse_hierarchy, "qltest_collapse_hierarchy"), - (defs.qltest.uncollapse_hierarchy, "qltest_uncollapse_hierarchy"), - (defs.cpp.skip, "cpp_skip"), - (defs.ql.internal, "ql_internal"), - ] +_class_pragmas = [ + (defs.qltest.collapse_hierarchy, "qltest_collapse_hierarchy"), + (defs.qltest.uncollapse_hierarchy, "qltest_uncollapse_hierarchy"), +] +_property_pragmas = [ + (defs.qltest.skip, "qltest_skip"), + (defs.cpp.skip, "cpp_skip"), + (defs.ql.internal, "ql_internal"), +] -@pytest.mark.parametrize("pragma,expected", _pragmas) +_pragmas = _class_pragmas + _property_pragmas + + +@pytest.mark.parametrize("pragma,expected", _property_pragmas) def test_property_with_pragma(pragma, expected): @load class data: @@ -293,7 +299,7 @@ class A: def test_property_with_pragmas(): spec = defs.string - for pragma, _ in _pragmas: + for pragma, _ in _property_pragmas: spec |= pragma @load @@ -303,7 +309,7 @@ class A: assert data.classes == { 'A': schema.Class('A', properties=[ - schema.SingleProperty('x', 'string', pragmas=[expected for _, expected in _pragmas]), + schema.SingleProperty('x', 'string', pragmas=[expected for _, expected in _property_pragmas]), ]), } @@ -349,8 +355,8 @@ class B(A): pass assert data.classes == { - 'A': schema.Class('A', derived={'B'}, synth=True), - 'B': schema.Class('B', bases=['A'], synth=schema.SynthInfo(from_class="A")), + 'A': schema.Class('A', derived={'B'}, pragmas={"synth": True}), + 'B': schema.Class('B', bases=['A'], pragmas={"synth": schema.SynthInfo(from_class="A")}), } @@ -365,7 +371,7 @@ class B(A): pass assert data.classes == { - 'A': schema.Class('A', derived={'B'}, synth=schema.SynthInfo(from_class="B")), + 'A': schema.Class('A', derived={'B'}, pragmas={"synth": schema.SynthInfo(from_class="B")}), 'B': schema.Class('B', bases=['A']), } @@ -390,8 +396,8 @@ class B(A): pass assert data.classes == { - 'A': schema.Class('A', derived={'B'}, synth=True), - 'B': schema.Class('B', bases=['A'], synth=schema.SynthInfo(on_arguments={'a': 'A', 'i': 'int'})), + 'A': schema.Class('A', derived={'B'}, pragmas={"synth": True}), + 'B': schema.Class('B', bases=['A'], pragmas={"synth": schema.SynthInfo(on_arguments={'a': 'A', 'i': 'int'})}), } @@ -409,7 +415,7 @@ class B(A): pass assert data.classes == { - 'A': schema.Class('A', derived={'B'}, synth=schema.SynthInfo(on_arguments={'b': 'B', 'i': 'int'})), + 'A': schema.Class('A', derived={'B'}, pragmas={"synth": schema.SynthInfo(on_arguments={'b': 'B', 'i': 'int'})}), 'B': schema.Class('B', bases=['A']), } @@ -448,10 +454,10 @@ class C(Root): assert data.classes == { 'Root': schema.Class('Root', derived={'Base', 'C'}), - 'Base': schema.Class('Base', bases=['Root'], derived={'Intermediate', 'B'}, synth=True), - 'Intermediate': schema.Class('Intermediate', bases=['Base'], derived={'A'}, synth=True), - 'A': schema.Class('A', bases=['Intermediate'], synth=schema.SynthInfo(on_arguments={'a': 'Base', 'i': 'int'})), - 'B': schema.Class('B', bases=['Base'], synth=schema.SynthInfo(from_class='Base')), + 'Base': schema.Class('Base', bases=['Root'], derived={'Intermediate', 'B'}, pragmas={"synth": True}), + 'Intermediate': schema.Class('Intermediate', bases=['Base'], derived={'A'}, pragmas={"synth": True}), + 'A': schema.Class('A', bases=['Intermediate'], pragmas={"synth": schema.SynthInfo(on_arguments={'a': 'Base', 'i': 'int'})}), + 'B': schema.Class('B', bases=['Base'], pragmas={"synth": schema.SynthInfo(from_class='Base')}), 'C': schema.Class('C', bases=['Root']), } @@ -636,7 +642,7 @@ class A: pass assert data.classes == { - 'A': schema.Class('A', default_doc_name="b"), + 'A': schema.Class('A', pragmas={"ql_default_doc_name": "b"}), } @@ -798,8 +804,7 @@ class _: pass assert data.classes == { - "Root": schema.Class("Root", hideable=True, - pragmas=["qltest_skip", "cpp_skip", "qltest_collapse_hierarchy"]), + "Root": schema.Class("Root", hideable=True, pragmas=["qltest_skip", "cpp_skip", "qltest_collapse_hierarchy"]), }