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).
This commit is contained in:
James Westman 2023-05-09 19:42:33 -05:00
parent aebf8be278
commit 04509e4b2e
31 changed files with 175 additions and 55 deletions

View file

@ -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")
):

View file

@ -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

View file

@ -23,6 +23,7 @@ from functools import cached_property
from .common import *
from .gobject_object import Object
from .gtkbuilder_template import Template
@dataclass
@ -51,8 +52,11 @@ class ScopeCtx:
if obj.tokens["id"] in passed:
token = obj.group.tokens["id"]
if not isinstance(obj, Template):
raise CompileError(
f"Duplicate object ID '{obj.tokens['id']}'", token.start, token.end
f"Duplicate object ID '{obj.tokens['id']}'",
token.start,
token.end,
)
passed[obj.tokens["id"]] = obj

View file

@ -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

View file

@ -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")

View file

@ -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} {{")
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:
ctx.print(f"template {klass} : {decompile.full_name(gir_class)} {{")
return gir_class
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)

View file

@ -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")],
)

View file

@ -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"]
)

View file

@ -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)

View file

@ -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:

View file

@ -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):

View file

@ -1,4 +1,4 @@
using Gtk 4.0;
template MyWidget : Gtk.Widget {}
template $MyWidget : Gtk.Widget {}
Gtk.Widget {}

View file

@ -1,3 +1,3 @@
using Gtk 4.0;
template TestTemplate : Gtk.NotARealClass {}
template $TestTemplate : Gtk.NotARealClass {}

View file

@ -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

View file

@ -1,7 +1,5 @@
using Gtk 4.0;
template GtkListItem {
Label {
label: bind GtkListItem.item.label;
}
$MyObject object {
foo: bind object.bar.baz;
}

View file

@ -1 +1 @@
5,34,5,Could not determine the type of the preceding expression
4,24,3,Could not determine the type of the preceding expression

View file

@ -0,0 +1,10 @@
using Gtk 4.0;
template TestTemplate : ApplicationWindow {
test-property: "Hello, world";
test-signal => $on_test_signal();
}
Dialog {
transient-for: TestTemplate;
}

View file

@ -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)

View file

@ -1,3 +1,3 @@
using Gtk 4.0;
template TestTemplate : Gtk.Orientable {}
template $TestTemplate : Gtk.Orientable {}

View file

@ -1 +1 @@
3,25,14,Gtk.Orientable is an interface, not a class
3,26,14,Gtk.Orientable is an interface, not a class

View file

@ -1,3 +1,3 @@
using Gtk 4.0;
template TestTemplate : Adw.ApplicationWindow {}
template $TestTemplate : Adw.ApplicationWindow {}

View file

@ -1 +1 @@
3,25,3,Namespace Adw was not imported
3,26,3,Namespace Adw was not imported

View file

@ -0,0 +1,3 @@
using Gtk 4.0;
template Gtk.ListItem : $WrongParent {}

View file

@ -0,0 +1 @@
3,1,39,Parent type may only be specified if the template type is extern

View file

@ -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 {}

View file

@ -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

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -1,3 +1,3 @@
using Gtk 4.0;
template GtkListItem {}
template Gtk.ListItem {}

View file

@ -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")