From 04509e4b2e924bcdc519fd8a53834d4bb78bddea Mon Sep 17 00:00:00 2001 From: James Westman Date: Tue, 9 May 2023 19:42:33 -0500 Subject: [PATCH] Change template syntax Templates now use a TypeName instead of an identifier, which makes it clearer that it's an extern symbol (or that it's a Gtk.ListItem). --- blueprintcompiler/decompiler.py | 3 + blueprintcompiler/gir.py | 6 +- blueprintcompiler/language/contexts.py | 10 +++- blueprintcompiler/language/expression.py | 6 +- blueprintcompiler/language/gobject_object.py | 6 +- .../language/gtkbuilder_template.py | 56 ++++++++++++------- .../language/property_binding.py | 10 ++++ blueprintcompiler/language/types.py | 39 +++++++++++++ blueprintcompiler/language/ui.py | 16 +++++- blueprintcompiler/language/values.py | 8 +++ blueprintcompiler/outputs/xml/__init__.py | 7 ++- tests/sample_errors/abstract_class.blp | 2 +- tests/sample_errors/class_dne.blp | 2 +- tests/sample_errors/class_dne.err | 2 +- tests/sample_errors/expr_cast_needed.blp | 6 +- tests/sample_errors/expr_cast_needed.err | 2 +- tests/sample_errors/legacy_template.blp | 10 ++++ tests/sample_errors/legacy_template.err | 2 + tests/sample_errors/not_a_class.blp | 2 +- tests/sample_errors/not_a_class.err | 2 +- tests/sample_errors/ns_not_imported.blp | 2 +- tests/sample_errors/ns_not_imported.err | 2 +- tests/sample_errors/template_parent.blp | 3 + tests/sample_errors/template_parent.err | 1 + tests/sample_errors/two_templates.blp | 6 +- tests/sample_errors/two_templates.err | 3 +- tests/samples/template.blp | 4 +- tests/samples/template_binding.blp | 4 +- tests/samples/template_binding_extern.blp | 4 +- tests/samples/template_no_parent.blp | 2 +- tests/test_samples.py | 2 + 31 files changed, 175 insertions(+), 55 deletions(-) create mode 100644 tests/sample_errors/legacy_template.blp create mode 100644 tests/sample_errors/legacy_template.err create mode 100644 tests/sample_errors/template_parent.blp create mode 100644 tests/sample_errors/template_parent.err diff --git a/blueprintcompiler/decompiler.py b/blueprintcompiler/decompiler.py index f7d858c..d3bf773 100644 --- a/blueprintcompiler/decompiler.py +++ b/blueprintcompiler/decompiler.py @@ -57,6 +57,7 @@ class DecompileCtx: self._indent: int = 0 self._blocks_need_end: T.List[str] = [] self._last_line_type: LineType = LineType.NONE + self.template_class: T.Optional[str] = None self.gir.add_namespace(get_namespace("Gtk", "4.0")) @@ -158,6 +159,8 @@ class DecompileCtx: ) ): self.print(f'{name}: "{escape_quote(value)}";') + elif value == self.template_class: + self.print(f"{name}: template;") elif type.assignable_to( self.gir.namespaces["Gtk"].lookup_type("GObject.Object") ): diff --git a/blueprintcompiler/gir.py b/blueprintcompiler/gir.py index fc415fd..e85190b 100644 --- a/blueprintcompiler/gir.py +++ b/blueprintcompiler/gir.py @@ -516,7 +516,7 @@ class Class(GirNode, GirType): class TemplateType(GirType): - def __init__(self, name: str, parent: T.Optional[Class]): + def __init__(self, name: str, parent: T.Optional[GirType]): self._name = name self.parent = parent @@ -534,14 +534,14 @@ class TemplateType(GirType): @cached_property def properties(self) -> T.Mapping[str, Property]: - if self.parent is None or isinstance(self.parent, ExternType): + if not (isinstance(self.parent, Class) or isinstance(self.parent, Interface)): return {} else: return self.parent.properties @cached_property def signals(self) -> T.Mapping[str, Signal]: - if self.parent is None or isinstance(self.parent, ExternType): + if not (isinstance(self.parent, Class) or isinstance(self.parent, Interface)): return {} else: return self.parent.signals diff --git a/blueprintcompiler/language/contexts.py b/blueprintcompiler/language/contexts.py index 1709918..069f4de 100644 --- a/blueprintcompiler/language/contexts.py +++ b/blueprintcompiler/language/contexts.py @@ -23,6 +23,7 @@ from functools import cached_property from .common import * from .gobject_object import Object +from .gtkbuilder_template import Template @dataclass @@ -51,9 +52,12 @@ class ScopeCtx: if obj.tokens["id"] in passed: token = obj.group.tokens["id"] - raise CompileError( - f"Duplicate object ID '{obj.tokens['id']}'", token.start, token.end - ) + if not isinstance(obj, Template): + raise CompileError( + f"Duplicate object ID '{obj.tokens['id']}'", + token.start, + token.end, + ) passed[obj.tokens["id"]] = obj def _iter_recursive(self, node: AstNode): diff --git a/blueprintcompiler/language/expression.py b/blueprintcompiler/language/expression.py index dd8db60..3f816f3 100644 --- a/blueprintcompiler/language/expression.py +++ b/blueprintcompiler/language/expression.py @@ -85,9 +85,9 @@ class LiteralExpr(ExprBase): def is_object(self) -> bool: from .values import IdentLiteral - return ( - isinstance(self.literal.value, IdentLiteral) - and self.literal.value.ident in self.context[ScopeCtx].objects + return isinstance(self.literal.value, IdentLiteral) and ( + self.literal.value.ident in self.context[ScopeCtx].objects + or self.root.is_legacy_template(self.literal.value.ident) ) @property diff --git a/blueprintcompiler/language/gobject_object.py b/blueprintcompiler/language/gobject_object.py index 9eeae3d..183ad8e 100644 --- a/blueprintcompiler/language/gobject_object.py +++ b/blueprintcompiler/language/gobject_object.py @@ -49,7 +49,7 @@ class Object(AstNode): return self.tokens["id"] @property - def class_name(self) -> T.Optional[ClassName]: + def class_name(self) -> ClassName: return self.children[ClassName][0] @property @@ -78,7 +78,9 @@ class Object(AstNode): @validate("id") def object_id_not_reserved(self): - if self.id in RESERVED_IDS: + from .gtkbuilder_template import Template + + if not isinstance(self, Template) and self.id in RESERVED_IDS: raise CompileWarning(f"{self.id} may be a confusing object ID") diff --git a/blueprintcompiler/language/gtkbuilder_template.py b/blueprintcompiler/language/gtkbuilder_template.py index 46ff6e6..149152a 100644 --- a/blueprintcompiler/language/gtkbuilder_template.py +++ b/blueprintcompiler/language/gtkbuilder_template.py @@ -19,15 +19,18 @@ import typing as T +from blueprintcompiler.language.common import GirType + from .gobject_object import Object, ObjectContent from .common import * -from .types import ClassName +from ..gir import TemplateType +from .types import ClassName, TemplateClassName class Template(Object): grammar = [ - "template", - UseIdent("id").expected("template class name"), + UseExact("id", "template"), + to_parse_node(TemplateClassName).expected("template type"), Optional( [ Match(":"), @@ -39,21 +42,29 @@ class Template(Object): @property def id(self) -> str: - return self.tokens["id"] + return "template" @property - def class_name(self) -> T.Optional[ClassName]: - if len(self.children[ClassName]): - return self.children[ClassName][0] + def gir_class(self) -> GirType: + if isinstance(self.class_name.gir_type, ExternType): + if gir := self.parent_type: + return TemplateType(self.class_name.gir_type.full_name, gir.gir_type) + return self.class_name.gir_type + + @property + def parent_type(self) -> T.Optional[ClassName]: + if len(self.children[ClassName]) == 2: + return self.children[ClassName][1] else: return None - @property - def gir_class(self): - if self.class_name is None: - return gir.TemplateType(self.id, None) - else: - return gir.TemplateType(self.id, self.class_name.gir_type) + @validate() + def parent_only_if_extern(self): + if not isinstance(self.class_name.gir_type, ExternType): + if self.parent_type is not None: + raise CompileError( + "Parent type may only be specified if the template type is extern" + ) @validate("id") def unique_in_parent(self): @@ -63,10 +74,15 @@ class Template(Object): @decompiler("template") -def decompile_template(ctx: DecompileCtx, gir, klass, parent="Widget"): - gir_class = ctx.type_by_cname(parent) - if gir_class is None: - ctx.print(f"template {klass} : .{parent} {{") - else: - ctx.print(f"template {klass} : {decompile.full_name(gir_class)} {{") - return gir_class +def decompile_template(ctx: DecompileCtx, gir, klass, parent=None): + def class_name(cname: str) -> str: + if gir := ctx.type_by_cname(cname): + return decompile.full_name(gir) + else: + return "$" + cname + + ctx.print(f"template {class_name(klass)} : {class_name(parent)} {{") + + ctx.template_class = klass + + return ctx.type_by_cname(klass) or ctx.type_by_cname(parent) diff --git a/blueprintcompiler/language/property_binding.py b/blueprintcompiler/language/property_binding.py index 200b271..13bec61 100644 --- a/blueprintcompiler/language/property_binding.py +++ b/blueprintcompiler/language/property_binding.py @@ -70,6 +70,8 @@ class PropertyBinding(AstNode): @property def source_obj(self) -> T.Optional[Object]: + if self.root.is_legacy_template(self.source): + return self.root.template return self.context[ScopeCtx].objects.get(self.source) @property @@ -127,3 +129,11 @@ class PropertyBinding(AstNode): "Use 'bind-property', introduced in blueprint 0.8.0, to use binding flags", actions=[CodeAction("Use 'bind-property'", "bind-property")], ) + + @validate("source") + def legacy_template(self): + if self.root.is_legacy_template(self.source): + raise UpgradeWarning( + "Use 'template' instead of the class name (introduced in 0.8.0)", + actions=[CodeAction("Use 'template'", "template")], + ) diff --git a/blueprintcompiler/language/types.py b/blueprintcompiler/language/types.py index 165f6b2..dbce44f 100644 --- a/blueprintcompiler/language/types.py +++ b/blueprintcompiler/language/types.py @@ -122,3 +122,42 @@ class ConcreteClassName(ClassName): f"{self.gir_type.full_name} can't be instantiated because it's abstract", hints=[f"did you mean to use a subclass of {self.gir_type.full_name}?"], ) + + +class TemplateClassName(ClassName): + """Handles the special case of a template type. The old syntax uses an identifier, + which is ambiguous with the new syntax. So this class displays an appropriate + upgrade warning instead of a class not found error.""" + + @property + def is_legacy(self): + return ( + self.tokens["extern"] is None + and self.tokens["namespace"] is None + and self.root.gir.get_type(self.tokens["class_name"], "Gtk") is None + ) + + @property + def gir_type(self) -> gir.GirType: + if self.is_legacy: + return gir.ExternType(self.tokens["class_name"]) + else: + return super().gir_type + + @validate("class_name") + def type_exists(self): + if self.is_legacy: + if type := self.root.gir.get_type_by_cname(self.tokens["class_name"]): + replacement = type.full_name + else: + replacement = "$" + self.tokens["class_name"] + + raise UpgradeWarning( + "Use type syntax here (introduced in blueprint 0.8.0)", + actions=[CodeAction("Use type syntax", replace_with=replacement)], + ) + + if not self.tokens["extern"] and self.gir_ns is not None: + self.root.gir.validate_type( + self.tokens["class_name"], self.tokens["namespace"] + ) diff --git a/blueprintcompiler/language/ui.py b/blueprintcompiler/language/ui.py index ea2dac8..033e2ca 100644 --- a/blueprintcompiler/language/ui.py +++ b/blueprintcompiler/language/ui.py @@ -43,7 +43,7 @@ class UI(AstNode): ] @property - def gir(self): + def gir(self) -> gir.GirContext: gir_ctx = gir.GirContext() self._gir_errors = [] @@ -84,6 +84,20 @@ class UI(AstNode): or isinstance(child, Menu) ] + @property + def template(self) -> T.Optional[Template]: + if len(self.children[Template]): + return self.children[Template][0] + else: + return None + + def is_legacy_template(self, id: str) -> bool: + return ( + id not in self.context[ScopeCtx].objects + and self.template is not None + and self.template.class_name.glib_type_name == id + ) + @context(ScopeCtx) def scope_ctx(self) -> ScopeCtx: return ScopeCtx(node=self) diff --git a/blueprintcompiler/language/values.py b/blueprintcompiler/language/values.py index f143f29..cf88249 100644 --- a/blueprintcompiler/language/values.py +++ b/blueprintcompiler/language/values.py @@ -260,6 +260,8 @@ class IdentLiteral(AstNode): return gir.BoolType() elif object := self.context[ScopeCtx].objects.get(self.ident): return object.gir_class + elif self.root.is_legacy_template(self.ident): + return self.root.template.class_name.gir_type else: return None @@ -277,6 +279,12 @@ class IdentLiteral(AstNode): did_you_mean=(self.ident, list(expected_type.members.keys())), ) + elif self.root.is_legacy_template(self.ident): + raise UpgradeWarning( + "Use 'template' instead of the class name (introduced in 0.8.0)", + actions=[CodeAction("Use 'template'", "template")], + ) + elif expected_type is not None: object = self.context[ScopeCtx].objects.get(self.ident) if object is None: diff --git a/blueprintcompiler/outputs/xml/__init__.py b/blueprintcompiler/outputs/xml/__init__.py index 1b5b579..3774c07 100644 --- a/blueprintcompiler/outputs/xml/__init__.py +++ b/blueprintcompiler/outputs/xml/__init__.py @@ -8,6 +8,7 @@ from .xml_emitter import XmlEmitter class XmlOutput(OutputFormat): def emit(self, ui: UI) -> str: xml = XmlEmitter() + self._ui = ui self._emit_ui(ui, xml) return xml.result @@ -32,7 +33,9 @@ class XmlOutput(OutputFormat): xml.put_self_closing("requires", lib="gtk", version=gtk.gir_namespace.version) def _emit_template(self, template: Template, xml: XmlEmitter): - xml.start_tag("template", **{"class": template.id}, parent=template.class_name) + xml.start_tag( + "template", **{"class": template.gir_class}, parent=template.parent_type + ) self._emit_object_or_template(template, xml) xml.end_tag() @@ -188,6 +191,8 @@ class XmlOutput(OutputFormat): xml.put_text(value.ident) elif isinstance(value_type, gir.Enumeration): xml.put_text(str(value_type.members[value.ident].value)) + elif value.ident == "template" and self._ui.template is not None: + xml.put_text(self._ui.template.gir_class.glib_type_name) else: xml.put_text(value.ident) elif isinstance(value, TypeLiteral): diff --git a/tests/sample_errors/abstract_class.blp b/tests/sample_errors/abstract_class.blp index c6ea8b9..dfb1819 100644 --- a/tests/sample_errors/abstract_class.blp +++ b/tests/sample_errors/abstract_class.blp @@ -1,4 +1,4 @@ using Gtk 4.0; -template MyWidget : Gtk.Widget {} +template $MyWidget : Gtk.Widget {} Gtk.Widget {} diff --git a/tests/sample_errors/class_dne.blp b/tests/sample_errors/class_dne.blp index b594268..2d57234 100644 --- a/tests/sample_errors/class_dne.blp +++ b/tests/sample_errors/class_dne.blp @@ -1,3 +1,3 @@ using Gtk 4.0; -template TestTemplate : Gtk.NotARealClass {} +template $TestTemplate : Gtk.NotARealClass {} diff --git a/tests/sample_errors/class_dne.err b/tests/sample_errors/class_dne.err index 57d74f8..8097027 100644 --- a/tests/sample_errors/class_dne.err +++ b/tests/sample_errors/class_dne.err @@ -1 +1 @@ -3,29,13,Namespace Gtk does not contain a type called NotARealClass +3,30,13,Namespace Gtk does not contain a type called NotARealClass diff --git a/tests/sample_errors/expr_cast_needed.blp b/tests/sample_errors/expr_cast_needed.blp index f647cb5..286261d 100644 --- a/tests/sample_errors/expr_cast_needed.blp +++ b/tests/sample_errors/expr_cast_needed.blp @@ -1,7 +1,5 @@ using Gtk 4.0; -template GtkListItem { - Label { - label: bind GtkListItem.item.label; - } +$MyObject object { + foo: bind object.bar.baz; } \ No newline at end of file diff --git a/tests/sample_errors/expr_cast_needed.err b/tests/sample_errors/expr_cast_needed.err index 51269d2..d50e713 100644 --- a/tests/sample_errors/expr_cast_needed.err +++ b/tests/sample_errors/expr_cast_needed.err @@ -1 +1 @@ -5,34,5,Could not determine the type of the preceding expression \ No newline at end of file +4,24,3,Could not determine the type of the preceding expression \ No newline at end of file diff --git a/tests/sample_errors/legacy_template.blp b/tests/sample_errors/legacy_template.blp new file mode 100644 index 0000000..8d2c57b --- /dev/null +++ b/tests/sample_errors/legacy_template.blp @@ -0,0 +1,10 @@ +using Gtk 4.0; + +template TestTemplate : ApplicationWindow { + test-property: "Hello, world"; + test-signal => $on_test_signal(); +} + +Dialog { + transient-for: TestTemplate; +} diff --git a/tests/sample_errors/legacy_template.err b/tests/sample_errors/legacy_template.err new file mode 100644 index 0000000..876b0cf --- /dev/null +++ b/tests/sample_errors/legacy_template.err @@ -0,0 +1,2 @@ +3,10,12,Use type syntax here (introduced in blueprint 0.8.0) +9,18,12,Use 'template' instead of the class name (introduced in 0.8.0) \ No newline at end of file diff --git a/tests/sample_errors/not_a_class.blp b/tests/sample_errors/not_a_class.blp index 0383154..62e2a63 100644 --- a/tests/sample_errors/not_a_class.blp +++ b/tests/sample_errors/not_a_class.blp @@ -1,3 +1,3 @@ using Gtk 4.0; -template TestTemplate : Gtk.Orientable {} +template $TestTemplate : Gtk.Orientable {} diff --git a/tests/sample_errors/not_a_class.err b/tests/sample_errors/not_a_class.err index c69f602..fb28391 100644 --- a/tests/sample_errors/not_a_class.err +++ b/tests/sample_errors/not_a_class.err @@ -1 +1 @@ -3,25,14,Gtk.Orientable is an interface, not a class +3,26,14,Gtk.Orientable is an interface, not a class diff --git a/tests/sample_errors/ns_not_imported.blp b/tests/sample_errors/ns_not_imported.blp index 9c81596..46b382f 100644 --- a/tests/sample_errors/ns_not_imported.blp +++ b/tests/sample_errors/ns_not_imported.blp @@ -1,3 +1,3 @@ using Gtk 4.0; -template TestTemplate : Adw.ApplicationWindow {} +template $TestTemplate : Adw.ApplicationWindow {} diff --git a/tests/sample_errors/ns_not_imported.err b/tests/sample_errors/ns_not_imported.err index 15a24d1..0b6263b 100644 --- a/tests/sample_errors/ns_not_imported.err +++ b/tests/sample_errors/ns_not_imported.err @@ -1 +1 @@ -3,25,3,Namespace Adw was not imported +3,26,3,Namespace Adw was not imported diff --git a/tests/sample_errors/template_parent.blp b/tests/sample_errors/template_parent.blp new file mode 100644 index 0000000..3a16ee3 --- /dev/null +++ b/tests/sample_errors/template_parent.blp @@ -0,0 +1,3 @@ +using Gtk 4.0; + +template Gtk.ListItem : $WrongParent {} \ No newline at end of file diff --git a/tests/sample_errors/template_parent.err b/tests/sample_errors/template_parent.err new file mode 100644 index 0000000..1fa5f04 --- /dev/null +++ b/tests/sample_errors/template_parent.err @@ -0,0 +1 @@ +3,1,39,Parent type may only be specified if the template type is extern \ No newline at end of file diff --git a/tests/sample_errors/two_templates.blp b/tests/sample_errors/two_templates.blp index a9c4e7c..cd898bf 100644 --- a/tests/sample_errors/two_templates.blp +++ b/tests/sample_errors/two_templates.blp @@ -1,4 +1,6 @@ using Gtk 4.0; -template ClassName : Gtk.Button {} -template ClassName2 : Gtk.Button {} +template $ClassName : Gtk.Button {} +template $ClassName2 : Gtk.Button {} + +Label template {} \ No newline at end of file diff --git a/tests/sample_errors/two_templates.err b/tests/sample_errors/two_templates.err index d085061..5d7488e 100644 --- a/tests/sample_errors/two_templates.err +++ b/tests/sample_errors/two_templates.err @@ -1 +1,2 @@ -4,10,10,Only one template may be defined per file, but this file contains 2 +6,7,8,Duplicate object ID 'template' +4,1,8,Only one template may be defined per file, but this file contains 2 \ No newline at end of file diff --git a/tests/samples/template.blp b/tests/samples/template.blp index a0ed5cc..fa2f041 100644 --- a/tests/samples/template.blp +++ b/tests/samples/template.blp @@ -1,10 +1,10 @@ using Gtk 4.0; -template TestTemplate : ApplicationWindow { +template $TestTemplate : ApplicationWindow { test-property: "Hello, world"; test-signal => $on_test_signal(); } Dialog { - transient-for: TestTemplate; + transient-for: template; } \ No newline at end of file diff --git a/tests/samples/template_binding.blp b/tests/samples/template_binding.blp index bbe6e8d..e295dc9 100644 --- a/tests/samples/template_binding.blp +++ b/tests/samples/template_binding.blp @@ -1,5 +1,5 @@ using Gtk 4.0; -template MyTemplate : Box { - prop1: bind MyTemplate.prop2 as <$MyObject>.prop3; +template $MyTemplate : Box { + prop1: bind template.prop2 as <$MyObject>.prop3; } \ No newline at end of file diff --git a/tests/samples/template_binding_extern.blp b/tests/samples/template_binding_extern.blp index 2e4728e..3c66ce0 100644 --- a/tests/samples/template_binding_extern.blp +++ b/tests/samples/template_binding_extern.blp @@ -1,5 +1,5 @@ using Gtk 4.0; -template MyTemplate : $MyParentClass { - prop1: bind MyTemplate.prop2 as <$MyObject>.prop3; +template $MyTemplate : $MyParentClass { + prop1: bind template.prop2 as <$MyObject>.prop3; } \ No newline at end of file diff --git a/tests/samples/template_no_parent.blp b/tests/samples/template_no_parent.blp index 2e3defc..1164998 100644 --- a/tests/samples/template_no_parent.blp +++ b/tests/samples/template_no_parent.blp @@ -1,3 +1,3 @@ using Gtk 4.0; -template GtkListItem {} +template Gtk.ListItem {} diff --git a/tests/test_samples.py b/tests/test_samples.py index 6e19a08..66faef1 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -266,6 +266,7 @@ class TestSamples(unittest.TestCase): self.assert_sample_error("inline_menu") self.assert_sample_error("invalid_bool") self.assert_sample_error("layout_in_non_widget") + self.assert_sample_error("legacy_template") self.assert_sample_error("menu_no_id") self.assert_sample_error("menu_toplevel_attribute") self.assert_sample_error("no_import_version") @@ -284,6 +285,7 @@ class TestSamples(unittest.TestCase): self.assert_sample_error("strv") self.assert_sample_error("styles_in_non_widget") self.assert_sample_error("subscope") + self.assert_sample_error("template_parent") self.assert_sample_error("two_templates") self.assert_sample_error("uint") self.assert_sample_error("using_invalid_namespace")