diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b924a7d..0295b09 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -18,7 +18,7 @@ build: - ninja -C _build docs/en - git clone https://gitlab.gnome.org/jwestman/blueprint-regression-tests.git - cd blueprint-regression-tests - - git checkout 58fda9381dac4a9c42c18a4b06149ed59ee702dc + - git checkout 59eecfbd73020889410da6cc9f5ce90e5b6f9e24 - ./test.sh - cd .. coverage: '/TOTAL.*\s([.\d]+)%/' diff --git a/blueprintcompiler/ast_utils.py b/blueprintcompiler/ast_utils.py index 16298ae..7bd5418 100644 --- a/blueprintcompiler/ast_utils.py +++ b/blueprintcompiler/ast_utils.py @@ -24,6 +24,8 @@ import typing as T from .errors import * from .lsp_utils import SemanticToken +TType = T.TypeVar("TType") + class Children: """Allows accessing children by type using array syntax.""" @@ -34,6 +36,14 @@ class Children: def __iter__(self): return iter(self._children) + @T.overload + def __getitem__(self, key: T.Type[TType]) -> T.List[TType]: + ... + + @T.overload + def __getitem__(self, key: int) -> "AstNode": + ... + def __getitem__(self, key): if isinstance(key, int): return self._children[key] @@ -41,6 +51,27 @@ class Children: return [child for child in self._children if isinstance(child, key)] +TCtx = T.TypeVar("TCtx") +TAttr = T.TypeVar("TAttr") + + +class Ctx: + """Allows accessing values from higher in the syntax tree.""" + + def __init__(self, node: "AstNode") -> None: + self.node = node + + def __getitem__(self, key: T.Type[TCtx]) -> T.Optional[TCtx]: + attrs = self.node._attrs_by_type(Context) + for name, attr in attrs: + if attr.type == key: + return getattr(self.node, name) + if self.node.parent is not None: + return self.node.parent.context[key] + else: + return None + + class AstNode: """Base class for nodes in the abstract syntax tree.""" @@ -62,6 +93,10 @@ class AstNode: getattr(cls, f) for f in dir(cls) if hasattr(getattr(cls, f), "_validator") ] + @cached_property + def context(self): + return Ctx(self) + @property def root(self): if self.parent is None: @@ -105,7 +140,9 @@ class AstNode: for child in self.children: yield from child._get_errors() - def _attrs_by_type(self, attr_type): + def _attrs_by_type( + self, attr_type: T.Type[TAttr] + ) -> T.Iterator[T.Tuple[str, TAttr]]: for name in dir(type(self)): item = getattr(type(self), name) if isinstance(item, attr_type): @@ -217,3 +254,23 @@ def docs(*args, **kwargs): return Docs(func, *args, **kwargs) return decorator + + +class Context: + def __init__(self, type: T.Type[TCtx], func: T.Callable[[AstNode], TCtx]) -> None: + self.type = type + self.func = func + + def __get__(self, instance, owner): + if instance is None: + return self + return self.func(instance) + + +def context(type: T.Type[TCtx]): + """Decorator for functions that return a context object, which is passed down to .""" + + def decorator(func: T.Callable[[AstNode], TCtx]) -> Context: + return Context(type, func) + + return decorator diff --git a/blueprintcompiler/decompiler.py b/blueprintcompiler/decompiler.py index c068c93..036c86f 100644 --- a/blueprintcompiler/decompiler.py +++ b/blueprintcompiler/decompiler.py @@ -295,7 +295,7 @@ def decompile_property( flags += " inverted" if "bidirectional" in bind_flags: flags += " bidirectional" - ctx.print(f"{name}: bind {bind_source}.{bind_property}{flags};") + ctx.print(f"{name}: bind-property {bind_source}.{bind_property}{flags};") elif truthy(translatable): if context is not None: ctx.print( diff --git a/blueprintcompiler/language/__init__.py b/blueprintcompiler/language/__init__.py index 4063943..822a91a 100644 --- a/blueprintcompiler/language/__init__.py +++ b/blueprintcompiler/language/__init__.py @@ -1,4 +1,6 @@ from .attributes import BaseAttribute, BaseTypedAttribute +from .binding import Binding +from .contexts import ValueTypeCtx from .expression import CastExpr, ClosureExpr, Expr, ExprChain, IdentExpr, LookupOp from .gobject_object import Object, ObjectContent from .gobject_property import Property @@ -14,16 +16,21 @@ from .gtk_styles import Styles from .gtkbuilder_child import Child from .gtkbuilder_template import Template from .imports import GtkDirective, Import +from .property_binding import PropertyBinding from .ui import UI from .types import ClassName from .values import ( - TypeValue, - IdentValue, - TranslatedStringValue, - FlagsValue, Flag, - QuotedValue, - NumberValue, + Flags, + IdentLiteral, + Literal, + NumberLiteral, + ObjectValue, + QuotedLiteral, + Translated, + TranslatedWithContext, + TranslatedWithoutContext, + TypeLiteral, Value, ) @@ -43,12 +50,3 @@ OBJECT_CONTENT_HOOKS.children = [ Strings, Child, ] - -VALUE_HOOKS.children = [ - TypeValue, - TranslatedStringValue, - FlagsValue, - IdentValue, - QuotedValue, - NumberValue, -] diff --git a/blueprintcompiler/language/attributes.py b/blueprintcompiler/language/attributes.py index 77f01f2..c713917 100644 --- a/blueprintcompiler/language/attributes.py +++ b/blueprintcompiler/language/attributes.py @@ -18,7 +18,7 @@ # SPDX-License-Identifier: LGPL-3.0-or-later -from .values import Value, TranslatedStringValue +from .values import Value, Translated from .common import * diff --git a/blueprintcompiler/language/binding.py b/blueprintcompiler/language/binding.py new file mode 100644 index 0000000..91d5a6b --- /dev/null +++ b/blueprintcompiler/language/binding.py @@ -0,0 +1,55 @@ +# binding.py +# +# Copyright 2023 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + +from dataclasses import dataclass + +from .common import * +from .expression import ExprChain, LookupOp, IdentExpr +from .contexts import ValueTypeCtx + + +class Binding(AstNode): + grammar = [ + Keyword("bind"), + ExprChain, + ] + + @property + def expression(self) -> ExprChain: + return self.children[ExprChain][0] + + @property + def simple_binding(self) -> T.Optional["SimpleBinding"]: + if isinstance(self.expression.last, LookupOp): + if isinstance(self.expression.last.lhs, IdentExpr): + return SimpleBinding( + self.expression.last.lhs.ident, self.expression.last.property_name + ) + return None + + @validate("bind") + def not_bindable(self) -> None: + if binding_error := self.context[ValueTypeCtx].binding_error: + raise binding_error + + +@dataclass +class SimpleBinding: + source: str + property_name: str diff --git a/blueprintcompiler/language/common.py b/blueprintcompiler/language/common.py index 8ae8d96..636f15d 100644 --- a/blueprintcompiler/language/common.py +++ b/blueprintcompiler/language/common.py @@ -19,7 +19,7 @@ from .. import gir -from ..ast_utils import AstNode, validate, docs +from ..ast_utils import AstNode, validate, docs, context from ..errors import ( CompileError, MultipleErrors, @@ -44,4 +44,3 @@ from ..parse_tree import * OBJECT_CONTENT_HOOKS = AnyOf() -VALUE_HOOKS = AnyOf() diff --git a/blueprintcompiler/language/contexts.py b/blueprintcompiler/language/contexts.py new file mode 100644 index 0000000..f5b92c2 --- /dev/null +++ b/blueprintcompiler/language/contexts.py @@ -0,0 +1,28 @@ +# contexts.py +# +# Copyright 2023 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + +import typing as T +from dataclasses import dataclass +from .common import * + + +@dataclass +class ValueTypeCtx: + value_type: T.Optional[GirType] + binding_error: T.Optional[CompileError] = None diff --git a/blueprintcompiler/language/expression.py b/blueprintcompiler/language/expression.py index f2b2ea1..e75b961 100644 --- a/blueprintcompiler/language/expression.py +++ b/blueprintcompiler/language/expression.py @@ -20,6 +20,7 @@ from .common import * from .types import TypeName +from .gtkbuilder_template import Template expr = Pratt() @@ -30,6 +31,10 @@ class Expr(AstNode): def type(self) -> T.Optional[GirType]: raise NotImplementedError() + @property + def type_complete(self) -> bool: + return True + @property def rhs(self) -> T.Optional["Expr"]: if isinstance(self.parent, ExprChain): @@ -53,6 +58,10 @@ class ExprChain(Expr): def type(self) -> T.Optional[GirType]: return self.last.type + @property + def type_complete(self) -> bool: + return self.last.type_complete + class InfixExpr(Expr): @property @@ -83,6 +92,13 @@ class IdentExpr(Expr): else: return None + @property + def type_complete(self) -> bool: + if object := self.root.objects_by_id.get(self.ident): + return not isinstance(object, Template) + else: + return True + class LookupOp(InfixExpr): grammar = [".", UseIdent("property")] @@ -103,17 +119,24 @@ class LookupOp(InfixExpr): @validate("property") def property_exists(self): - if self.lhs.type is None or isinstance(self.lhs.type, UncheckedType): + if ( + self.lhs.type is None + or not self.lhs.type_complete + or isinstance(self.lhs.type, UncheckedType) + ): return + elif not isinstance(self.lhs.type, gir.Class) and not isinstance( self.lhs.type, gir.Interface ): raise CompileError( f"Type {self.lhs.type.full_name} does not have properties" ) + elif self.lhs.type.properties.get(self.property_name) is None: raise CompileError( - f"{self.lhs.type.full_name} does not have a property called {self.property_name}" + f"{self.lhs.type.full_name} does not have a property called {self.property_name}", + did_you_mean=(self.property_name, self.lhs.type.properties.keys()), ) @@ -124,9 +147,13 @@ class CastExpr(InfixExpr): def type(self) -> T.Optional[GirType]: return self.children[TypeName][0].gir_type + @property + def type_complete(self) -> bool: + return True + @validate() def cast_makes_sense(self): - if self.lhs.type is None: + if self.type is None or self.lhs.type is None: return if not self.type.assignable_to(self.lhs.type): diff --git a/blueprintcompiler/language/gobject_property.py b/blueprintcompiler/language/gobject_property.py index 10770a8..13374f8 100644 --- a/blueprintcompiler/language/gobject_property.py +++ b/blueprintcompiler/language/gobject_property.py @@ -17,51 +17,28 @@ # # SPDX-License-Identifier: LGPL-3.0-or-later +from dataclasses import dataclass from .expression import ExprChain from .gobject_object import Object from .gtkbuilder_template import Template -from .values import Value, TranslatedStringValue +from .values import Value, Translated from .common import * +from .contexts import ValueTypeCtx +from .property_binding import PropertyBinding +from .binding import Binding class Property(AstNode): - grammar = AnyOf( - [ - UseIdent("name"), - ":", - Keyword("bind"), - UseIdent("bind_source"), - ".", - UseIdent("bind_property"), - ZeroOrMore( - AnyOf( - ["no-sync-create", UseLiteral("no_sync_create", True)], - ["inverted", UseLiteral("inverted", True)], - ["bidirectional", UseLiteral("bidirectional", True)], - Match("sync-create").warn( - "sync-create is deprecated in favor of no-sync-create" - ), - ) - ), - ";", - ], - Statement( - UseIdent("name"), - UseLiteral("binding", True), - ":", - "bind", - ExprChain, - ), - Statement( - UseIdent("name"), - ":", - AnyOf( - Object, - VALUE_HOOKS, - ).expected("a value"), - ), - ) + grammar = [UseIdent("name"), ":", Value, ";"] + + @property + def name(self) -> str: + return self.tokens["name"] + + @property + def value(self) -> Value: + return self.children[0] @property def gir_class(self): @@ -72,10 +49,29 @@ class Property(AstNode): if self.gir_class is not None and not isinstance(self.gir_class, UncheckedType): return self.gir_class.properties.get(self.tokens["name"]) - @property - def value_type(self): + @context(ValueTypeCtx) + def value_type(self) -> ValueTypeCtx: + if ( + ( + isinstance(self.value.child, PropertyBinding) + or isinstance(self.value.child, Binding) + ) + and self.gir_property is not None + and self.gir_property.construct_only + ): + binding_error = CompileError( + f"{self.gir_property.full_name} can't be bound because it is construct-only", + hints=["construct-only properties may only be set to a static value"], + ) + else: + binding_error = None + if self.gir_property is not None: - return self.gir_property.type + type = self.gir_property.type + else: + type = None + + return ValueTypeCtx(type, binding_error) @validate("name") def property_exists(self): @@ -95,40 +91,11 @@ class Property(AstNode): did_you_mean=(self.tokens["name"], self.gir_class.properties.keys()), ) - @validate("bind") - def property_bindable(self): - if ( - self.tokens["bind"] - and self.gir_property is not None - and self.gir_property.construct_only - ): - raise CompileError( - f"{self.gir_property.full_name} can't be bound because it is construct-only", - hints=["construct-only properties may only be set to a static value"], - ) - @validate("name") def property_writable(self): if self.gir_property is not None and not self.gir_property.writable: raise CompileError(f"{self.gir_property.full_name} is not writable") - @validate() - def obj_property_type(self): - if len(self.children[Object]) == 0: - return - - object = self.children[Object][0] - type = self.value_type - if ( - object - and type - and object.gir_class - and not object.gir_class.assignable_to(type) - ): - raise CompileError( - f"Cannot assign {object.gir_class.full_name} to {type.full_name}" - ) - @validate("name") def unique_in_parent(self): self.validate_unique_in_parent( diff --git a/blueprintcompiler/language/gobject_signal.py b/blueprintcompiler/language/gobject_signal.py index 74d7472..0c649b7 100644 --- a/blueprintcompiler/language/gobject_signal.py +++ b/blueprintcompiler/language/gobject_signal.py @@ -82,10 +82,11 @@ class Signal(AstNode): @validate("handler") def old_extern(self): if not self.tokens["extern"]: - raise UpgradeWarning( - "Use the '$' extern syntax introduced in blueprint 0.8.0", - actions=[CodeAction("Use '$' syntax", "$" + self.tokens["handler"])], - ) + if self.handler is not None: + raise UpgradeWarning( + "Use the '$' extern syntax introduced in blueprint 0.8.0", + actions=[CodeAction("Use '$' syntax", "$" + self.handler)], + ) @validate("name") def signal_exists(self): diff --git a/blueprintcompiler/language/gtk_a11y.py b/blueprintcompiler/language/gtk_a11y.py index f2066ff..a3e1888 100644 --- a/blueprintcompiler/language/gtk_a11y.py +++ b/blueprintcompiler/language/gtk_a11y.py @@ -21,6 +21,7 @@ from .gobject_object import ObjectContent, validate_parent_type from .attributes import BaseTypedAttribute from .values import Value from .common import * +from .contexts import ValueTypeCtx def get_property_types(gir): @@ -108,7 +109,7 @@ class A11yProperty(BaseTypedAttribute): grammar = Statement( UseIdent("name"), ":", - VALUE_HOOKS.expected("a value"), + Value, ) @property @@ -129,8 +130,12 @@ class A11yProperty(BaseTypedAttribute): return self.tokens["name"].replace("_", "-") @property - def value_type(self) -> GirType: - return get_types(self.root.gir).get(self.tokens["name"]) + def value(self) -> Value: + return self.children[0] + + @context(ValueTypeCtx) + def value_type(self) -> ValueTypeCtx: + return ValueTypeCtx(get_types(self.root.gir).get(self.tokens["name"])) @validate("name") def is_valid_property(self): @@ -161,6 +166,10 @@ class A11y(AstNode): Until(A11yProperty, "}"), ] + @property + def properties(self) -> T.List[A11yProperty]: + return self.children[A11yProperty] + @validate("accessibility") def container_is_widget(self): validate_parent_type(self, "Gtk", "Widget", "accessibility properties") diff --git a/blueprintcompiler/language/gtk_combo_box_text.py b/blueprintcompiler/language/gtk_combo_box_text.py index f0f6f37..e1a8a12 100644 --- a/blueprintcompiler/language/gtk_combo_box_text.py +++ b/blueprintcompiler/language/gtk_combo_box_text.py @@ -21,15 +21,22 @@ from .attributes import BaseTypedAttribute from .gobject_object import ObjectContent, validate_parent_type from .common import * +from .contexts import ValueTypeCtx +from .values import Value -class Item(BaseTypedAttribute): - tag_name = "item" - attr_name = "id" +class Item(AstNode): + @property + def name(self) -> str: + return self.tokens["name"] @property - def value_type(self): - return StringType() + def value(self) -> Value: + return self.children[Value][0] + + @context(ValueTypeCtx) + def value_type(self) -> ValueTypeCtx: + return ValueTypeCtx(StringType()) item = Group( @@ -41,7 +48,7 @@ item = Group( ":", ] ), - VALUE_HOOKS, + Value, ], ) diff --git a/blueprintcompiler/language/gtk_layout.py b/blueprintcompiler/language/gtk_layout.py index 9af82fd..4862148 100644 --- a/blueprintcompiler/language/gtk_layout.py +++ b/blueprintcompiler/language/gtk_layout.py @@ -21,15 +21,25 @@ from .attributes import BaseAttribute from .gobject_object import ObjectContent, validate_parent_type from .common import * +from .contexts import ValueTypeCtx +from .values import Value -class LayoutProperty(BaseAttribute): +class LayoutProperty(AstNode): tag_name = "property" @property - def value_type(self): + def name(self) -> str: + return self.tokens["name"] + + @property + def value(self) -> Value: + return self.children[Value][0] + + @context(ValueTypeCtx) + def value_type(self) -> ValueTypeCtx: # there isn't really a way to validate these - return None + return ValueTypeCtx(None) @validate("name") def unique_in_parent(self): @@ -41,11 +51,7 @@ class LayoutProperty(BaseAttribute): layout_prop = Group( LayoutProperty, - Statement( - UseIdent("name"), - ":", - VALUE_HOOKS.expected("a value"), - ), + Statement(UseIdent("name"), ":", Err(Value, "Expected a value")), ) diff --git a/blueprintcompiler/language/gtk_menu.py b/blueprintcompiler/language/gtk_menu.py index dedf6e3..df4b031 100644 --- a/blueprintcompiler/language/gtk_menu.py +++ b/blueprintcompiler/language/gtk_menu.py @@ -24,6 +24,7 @@ from blueprintcompiler.language.values import Value from .attributes import BaseAttribute from .gobject_object import Object, ObjectContent from .common import * +from .contexts import ValueTypeCtx class Menu(AstNode): @@ -49,17 +50,23 @@ class Menu(AstNode): raise CompileError("Menu requires an ID") -class MenuAttribute(BaseAttribute): +class MenuAttribute(AstNode): tag_name = "attribute" @property - def value_type(self): - return None + def name(self) -> str: + return self.tokens["name"] @property def value(self) -> Value: return self.children[Value][0] + @context(ValueTypeCtx) + def value_type(self) -> ValueTypeCtx: + return ValueTypeCtx( + None, binding_error=CompileError("Bindings are not permitted in menus") + ) + menu_contents = Sequence() @@ -78,7 +85,7 @@ menu_attribute = Group( [ UseIdent("name"), ":", - VALUE_HOOKS.expected("a value"), + Err(Value, "Expected a value"), Match(";").expected(), ], ) @@ -102,7 +109,7 @@ menu_item_shorthand = Group( "(", Group( MenuAttribute, - [UseLiteral("name", "label"), VALUE_HOOKS], + [UseLiteral("name", "label"), Value], ), Optional( [ @@ -111,14 +118,14 @@ menu_item_shorthand = Group( [ Group( MenuAttribute, - [UseLiteral("name", "action"), VALUE_HOOKS], + [UseLiteral("name", "action"), Value], ), Optional( [ ",", Group( MenuAttribute, - [UseLiteral("name", "icon"), VALUE_HOOKS], + [UseLiteral("name", "icon"), Value], ), ] ), diff --git a/blueprintcompiler/language/gtk_string_list.py b/blueprintcompiler/language/gtk_string_list.py index 347b9e8..b07fa69 100644 --- a/blueprintcompiler/language/gtk_string_list.py +++ b/blueprintcompiler/language/gtk_string_list.py @@ -20,16 +20,17 @@ from .attributes import BaseTypedAttribute from .gobject_object import ObjectContent, validate_parent_type -from .values import Value, TranslatedStringValue +from .values import Value, Translated from .common import * +from .contexts import ValueTypeCtx class Item(AstNode): - grammar = VALUE_HOOKS + grammar = Value - @property - def value_type(self): - return StringType() + @context(ValueTypeCtx) + def value_type(self) -> ValueTypeCtx: + return ValueTypeCtx(StringType()) class Strings(AstNode): diff --git a/blueprintcompiler/language/imports.py b/blueprintcompiler/language/imports.py index be6a003..224e0a3 100644 --- a/blueprintcompiler/language/imports.py +++ b/blueprintcompiler/language/imports.py @@ -64,6 +64,9 @@ class GtkDirective(AstNode): self.gtk_version() if self.tokens["version"] is not None: return gir.get_namespace("Gtk", self.tokens["version"]) + else: + # For better error handling, just assume it's 4.0 + return gir.get_namespace("Gtk", "4.0") class Import(AstNode): diff --git a/blueprintcompiler/language/property_binding.py b/blueprintcompiler/language/property_binding.py new file mode 100644 index 0000000..37a5c91 --- /dev/null +++ b/blueprintcompiler/language/property_binding.py @@ -0,0 +1,139 @@ +# property_binding.py +# +# Copyright 2023 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + +from .common import * +from .contexts import ValueTypeCtx +from .gobject_object import Object +from .gtkbuilder_template import Template + + +class PropertyBindingFlag(AstNode): + grammar = [ + AnyOf( + UseExact("flag", "inverted"), + UseExact("flag", "bidirectional"), + UseExact("flag", "no-sync-create"), + UseExact("flag", "sync-create"), + ) + ] + + @property + def flag(self) -> str: + return self.tokens["flag"] + + @validate() + def sync_create(self): + if self.flag == "sync-create": + raise UpgradeWarning( + "'sync-create' is now the default. Use 'no-sync-create' if this is not wanted.", + actions=[CodeAction("remove 'sync-create'", "")], + ) + + +class PropertyBinding(AstNode): + grammar = AnyOf( + [ + Keyword("bind-property"), + UseIdent("source"), + ".", + UseIdent("property"), + ZeroOrMore(PropertyBindingFlag), + ], + [ + Keyword("bind"), + UseIdent("source"), + ".", + UseIdent("property"), + PropertyBindingFlag, + ZeroOrMore(PropertyBindingFlag), + ], + ) + + @property + def source(self) -> str: + return self.tokens["source"] + + @property + def source_obj(self) -> T.Optional[Object]: + return self.root.objects_by_id.get(self.source) + + @property + def property_name(self) -> str: + return self.tokens["property"] + + @property + def flags(self) -> T.List[PropertyBindingFlag]: + return self.children[PropertyBindingFlag] + + @property + def inverted(self) -> bool: + return any([f.flag == "inverted" for f in self.flags]) + + @property + def bidirectional(self) -> bool: + return any([f.flag == "bidirectional" for f in self.flags]) + + @property + def no_sync_create(self) -> bool: + return any([f.flag == "no-sync-create" for f in self.flags]) + + @validate("source") + def source_object_exists(self) -> None: + if self.source_obj is None: + raise CompileError( + f"Could not find object with ID {self.source}", + did_you_mean=(self.source, self.root.objects_by_id.keys()), + ) + + @validate("property") + def property_exists(self) -> None: + if self.source_obj is None: + return + + gir_class = self.source_obj.gir_class + + if ( + isinstance(self.source_obj, Template) + or gir_class is None + or isinstance(gir_class, UncheckedType) + ): + # Objects that we have no gir data on should not be validated + # This happens for classes defined by the app itself + return + + if ( + isinstance(gir_class, gir.Class) + and gir_class.properties.get(self.property_name) is None + ): + raise CompileError( + f"{gir_class.full_name} does not have a property called {self.property_name}" + ) + + @validate("bind-property") + def not_bindable(self) -> None: + if binding_error := self.context[ValueTypeCtx].binding_error: + raise binding_error + + @validate("bind") + def old_bind(self): + if self.tokens["bind"]: + raise UpgradeWarning( + "Use 'bind-property', introduced in blueprint 0.8.0, to use binding flags", + actions=[CodeAction("Use 'bind-property'", "bind-property")], + ) diff --git a/blueprintcompiler/language/types.py b/blueprintcompiler/language/types.py index e1e715d..702ed32 100644 --- a/blueprintcompiler/language/types.py +++ b/blueprintcompiler/language/types.py @@ -94,7 +94,7 @@ class ClassName(TypeName): @validate("namespace", "class_name") def gir_class_exists(self): if ( - self.gir_type + self.gir_type is not None and not isinstance(self.gir_type, UncheckedType) and not isinstance(self.gir_type, Class) ): diff --git a/blueprintcompiler/language/values.py b/blueprintcompiler/language/values.py index 8447267..4300c39 100644 --- a/blueprintcompiler/language/values.py +++ b/blueprintcompiler/language/values.py @@ -21,41 +21,57 @@ import typing as T from .common import * from .types import TypeName +from .property_binding import PropertyBinding +from .binding import Binding +from .gobject_object import Object +from .contexts import ValueTypeCtx -class Value(AstNode): - pass - - -class TranslatedStringValue(Value): - grammar = AnyOf( - [ - "_", - "(", - UseQuoted("value").expected("a quoted string"), - Match(")").expected(), - ], - [ - "C_", - "(", - UseQuoted("context").expected("a quoted string"), - ",", - UseQuoted("value").expected("a quoted string"), - Optional(","), - Match(")").expected(), - ], - ) +class TranslatedWithoutContext(AstNode): + grammar = ["_", "(", UseQuoted("string"), Optional(","), ")"] @property def string(self) -> str: - return self.tokens["value"] + return self.tokens["string"] + + +class TranslatedWithContext(AstNode): + grammar = [ + "C_", + "(", + UseQuoted("context"), + ",", + UseQuoted("string"), + Optional(","), + ")", + ] @property - def context(self) -> T.Optional[str]: + def string(self) -> str: + return self.tokens["string"] + + @property + def context(self) -> str: return self.tokens["context"] -class TypeValue(Value): +class Translated(AstNode): + grammar = AnyOf(TranslatedWithoutContext, TranslatedWithContext) + + @property + def child(self) -> T.Union[TranslatedWithContext, TranslatedWithoutContext]: + return self.children[0] + + @validate() + def validate_for_type(self) -> None: + expected_type = self.context[ValueTypeCtx].value_type + if expected_type is not None and not expected_type.assignable_to(StringType()): + raise CompileError( + f"Cannot convert translated string to {expected_type.full_name}" + ) + + +class TypeLiteral(AstNode): grammar = [ "typeof", "(", @@ -64,17 +80,17 @@ class TypeValue(Value): ] @property - def type_name(self): + def type_name(self) -> TypeName: return self.children[TypeName][0] @validate() - def validate_for_type(self): - type = self.parent.value_type - if type is not None and not isinstance(type, gir.TypeType): - raise CompileError(f"Cannot convert GType to {type.full_name}") + def validate_for_type(self) -> None: + expected_type = self.context[ValueTypeCtx].value_type + if expected_type is not None and not isinstance(expected_type, gir.TypeType): + raise CompileError(f"Cannot convert GType to {expected_type.full_name}") -class QuotedValue(Value): +class QuotedLiteral(AstNode): grammar = UseQuoted("value") @property @@ -82,22 +98,22 @@ class QuotedValue(Value): return self.tokens["value"] @validate() - def validate_for_type(self): - type = self.parent.value_type + def validate_for_type(self) -> None: + expected_type = self.context[ValueTypeCtx].value_type if ( - isinstance(type, gir.IntType) - or isinstance(type, gir.UIntType) - or isinstance(type, gir.FloatType) + isinstance(expected_type, gir.IntType) + or isinstance(expected_type, gir.UIntType) + or isinstance(expected_type, gir.FloatType) ): raise CompileError(f"Cannot convert string to number") - elif isinstance(type, gir.StringType): + elif isinstance(expected_type, gir.StringType): pass elif ( - isinstance(type, gir.Class) - or isinstance(type, gir.Interface) - or isinstance(type, gir.Boxed) + isinstance(expected_type, gir.Class) + or isinstance(expected_type, gir.Interface) + or isinstance(expected_type, gir.Boxed) ): parseable_types = [ "Gdk.Paintable", @@ -111,31 +127,32 @@ class QuotedValue(Value): "Gsk.Transform", "GLib.Variant", ] - if type.full_name not in parseable_types: + if expected_type.full_name not in parseable_types: hints = [] - if isinstance(type, gir.TypeType): - hints.append( - f"use the typeof operator: 'typeof({self.tokens('value')})'" - ) + if isinstance(expected_type, gir.TypeType): + hints.append(f"use the typeof operator: 'typeof({self.value})'") raise CompileError( - f"Cannot convert string to {type.full_name}", hints=hints + f"Cannot convert string to {expected_type.full_name}", hints=hints ) - elif type is not None: - raise CompileError(f"Cannot convert string to {type.full_name}") + elif expected_type is not None: + raise CompileError(f"Cannot convert string to {expected_type.full_name}") -class NumberValue(Value): - grammar = UseNumber("value") +class NumberLiteral(AstNode): + grammar = [ + Optional(AnyOf(UseExact("sign", "-"), UseExact("sign", "+"))), + UseNumber("value"), + ] @property def value(self) -> T.Union[int, float]: return self.tokens["value"] @validate() - def validate_for_type(self): - type = self.parent.value_type - if isinstance(type, gir.IntType): + def validate_for_type(self) -> None: + expected_type = self.context[ValueTypeCtx].value_type + if isinstance(expected_type, gir.IntType): try: int(self.tokens["value"]) except: @@ -143,7 +160,7 @@ class NumberValue(Value): f"Cannot convert {self.group.tokens['value']} to integer" ) - elif isinstance(type, gir.UIntType): + elif isinstance(expected_type, gir.UIntType): try: int(self.tokens["value"]) if int(self.tokens["value"]) < 0: @@ -153,7 +170,7 @@ class NumberValue(Value): f"Cannot convert {self.group.tokens['value']} to unsigned integer" ) - elif isinstance(type, gir.FloatType): + elif isinstance(expected_type, gir.FloatType): try: float(self.tokens["value"]) except: @@ -161,8 +178,8 @@ class NumberValue(Value): f"Cannot convert {self.group.tokens['value']} to float" ) - elif type is not None: - raise CompileError(f"Cannot convert number to {type.full_name}") + elif expected_type is not None: + raise CompileError(f"Cannot convert number to {expected_type.full_name}") class Flag(AstNode): @@ -174,17 +191,17 @@ class Flag(AstNode): @property def value(self) -> T.Optional[int]: - type = self.parent.parent.value_type + type = self.context[ValueTypeCtx].value_type if not isinstance(type, Enumeration): return None - elif member := type.members.get(self.tokens["value"]): + elif member := type.members.get(self.name): return member.value else: return None @docs() def docs(self): - type = self.parent.parent.value_type + type = self.context[ValueTypeCtx].value_type if not isinstance(type, Enumeration): return if member := type.members.get(self.tokens["value"]): @@ -192,15 +209,18 @@ class Flag(AstNode): @validate() def validate_for_type(self): - type = self.parent.parent.value_type - if isinstance(type, gir.Bitfield) and self.tokens["value"] not in type.members: + expected_type = self.context[ValueTypeCtx].value_type + if ( + isinstance(expected_type, gir.Bitfield) + and self.tokens["value"] not in expected_type.members + ): raise CompileError( - f"{self.tokens['value']} is not a member of {type.full_name}", - did_you_mean=(self.tokens["value"], type.members.keys()), + f"{self.tokens['value']} is not a member of {expected_type.full_name}", + did_you_mean=(self.tokens["value"], expected_type.members.keys()), ) -class FlagsValue(Value): +class Flags(AstNode): grammar = [Flag, "|", Delimited(Flag, "|")] @property @@ -208,57 +228,104 @@ class FlagsValue(Value): return self.children @validate() - def parent_is_bitfield(self): - type = self.parent.value_type - if type is not None and not isinstance(type, gir.Bitfield): - raise CompileError(f"{type.full_name} is not a bitfield type") + def validate_for_type(self) -> None: + expected_type = self.context[ValueTypeCtx].value_type + if expected_type is not None and not isinstance(expected_type, gir.Bitfield): + raise CompileError(f"{expected_type.full_name} is not a bitfield type") -class IdentValue(Value): +class IdentLiteral(AstNode): grammar = UseIdent("value") + @property + def ident(self) -> str: + return self.tokens["value"] + @validate() - def validate_for_type(self): - type = self.parent.value_type + def validate_for_type(self) -> None: + expected_type = self.context[ValueTypeCtx].value_type + if isinstance(expected_type, gir.BoolType): + if self.ident not in ["true", "false"]: + raise CompileError(f"Expected 'true' or 'false' for boolean value") - if isinstance(type, gir.Enumeration): - if self.tokens["value"] not in type.members: + elif isinstance(expected_type, gir.Enumeration): + if self.ident not in expected_type.members: raise CompileError( - f"{self.tokens['value']} is not a member of {type.full_name}", - did_you_mean=(self.tokens["value"], type.members.keys()), + f"{self.ident} is not a member of {expected_type.full_name}", + did_you_mean=(self.ident, list(expected_type.members.keys())), ) - elif isinstance(type, gir.BoolType): - if self.tokens["value"] not in ["true", "false"]: - raise CompileError( - f"Expected 'true' or 'false' for boolean value", - did_you_mean=(self.tokens["value"], ["true", "false"]), - ) - - elif type is not None: - object = self.root.objects_by_id.get(self.tokens["value"]) + elif expected_type is not None: + object = self.root.objects_by_id.get(self.ident) if object is None: raise CompileError( - f"Could not find object with ID {self.tokens['value']}", - did_you_mean=(self.tokens["value"], self.root.objects_by_id.keys()), + f"Could not find object with ID {self.ident}", + did_you_mean=(self.ident, self.root.objects_by_id.keys()), ) - elif object.gir_class and not object.gir_class.assignable_to(type): + elif object.gir_class and not object.gir_class.assignable_to(expected_type): raise CompileError( - f"Cannot assign {object.gir_class.full_name} to {type.full_name}" + f"Cannot assign {object.gir_class.full_name} to {expected_type.full_name}" ) @docs() - def docs(self): - type = self.parent.value_type + def docs(self) -> T.Optional[str]: + type = self.context[ValueTypeCtx].value_type if isinstance(type, gir.Enumeration): - if member := type.members.get(self.tokens["value"]): + if member := type.members.get(self.ident): return member.doc else: return type.doc elif isinstance(type, gir.GirNode): return type.doc + else: + return None def get_semantic_tokens(self) -> T.Iterator[SemanticToken]: if isinstance(self.parent.value_type, gir.Enumeration): token = self.group.tokens["value"] yield SemanticToken(token.start, token.end, SemanticTokenType.EnumMember) + + +class Literal(AstNode): + grammar = AnyOf( + TypeLiteral, + QuotedLiteral, + NumberLiteral, + IdentLiteral, + ) + + @property + def value( + self, + ) -> T.Union[TypeLiteral, QuotedLiteral, NumberLiteral, IdentLiteral]: + return self.children[0] + + +class ObjectValue(AstNode): + grammar = Object + + @property + def object(self) -> Object: + return self.children[Object][0] + + @validate() + def validate_for_type(self) -> None: + expected_type = self.context[ValueTypeCtx].value_type + if ( + expected_type is not None + and self.object.gir_class is not None + and not self.object.gir_class.assignable_to(expected_type) + ): + raise CompileError( + f"Cannot assign {self.object.gir_class.full_name} to {expected_type.full_name}" + ) + + +class Value(AstNode): + grammar = AnyOf(PropertyBinding, Binding, Translated, ObjectValue, Flags, Literal) + + @property + def child( + self, + ) -> T.Union[PropertyBinding, Binding, Translated, ObjectValue, Flags, Literal,]: + return self.children[0] diff --git a/blueprintcompiler/outputs/xml/__init__.py b/blueprintcompiler/outputs/xml/__init__.py index 48a18ac..c38c070 100644 --- a/blueprintcompiler/outputs/xml/__init__.py +++ b/blueprintcompiler/outputs/xml/__init__.py @@ -82,51 +82,57 @@ class XmlOutput(OutputFormat): xml.end_tag() def _emit_property(self, property: Property, xml: XmlEmitter): - values = property.children[Value] - value = values[0] if len(values) == 1 else None + value = property.value + child = value.child - bind_flags = [] - if property.tokens["bind_source"] and not property.tokens["no_sync_create"]: - bind_flags.append("sync-create") - if property.tokens["inverted"]: - bind_flags.append("invert-boolean") - if property.tokens["bidirectional"]: - bind_flags.append("bidirectional") - bind_flags_str = "|".join(bind_flags) or None - - props = { - "name": property.tokens["name"], - "bind-source": property.tokens["bind_source"], - "bind-property": property.tokens["bind_property"], - "bind-flags": bind_flags_str, + props: T.Dict[str, T.Optional[str]] = { + "name": property.name, } - if isinstance(value, TranslatedStringValue): - xml.start_tag("property", **props, **self._translated_string_attrs(value)) - xml.put_text(value.string) + if isinstance(child, Translated): + xml.start_tag("property", **props, **self._translated_string_attrs(child)) + xml.put_text(child.child.string) xml.end_tag() - elif len(property.children[Object]) == 1: + elif isinstance(child, Object): xml.start_tag("property", **props) - self._emit_object(property.children[Object][0], xml) + self._emit_object(child, xml) xml.end_tag() - elif value is None: - if property.tokens["binding"]: - xml.start_tag("binding", **props) - self._emit_expression(property.children[ExprChain][0], xml) - xml.end_tag() - else: + elif isinstance(child, Binding): + if simple := child.simple_binding: + props["bind-source"] = simple.source + props["bind-property"] = simple.property_name + props["bind-flags"] = "sync-create" xml.put_self_closing("property", **props) + else: + xml.start_tag("binding", **props) + self._emit_expression(child.expression, xml) + xml.end_tag() + elif isinstance(child, PropertyBinding): + bind_flags = [] + if not child.no_sync_create: + bind_flags.append("sync-create") + if child.inverted: + bind_flags.append("invert-boolean") + if child.bidirectional: + bind_flags.append("bidirectional") + + props["bind-source"] = child.source + props["bind-property"] = child.property_name + props["bind-flags"] = "|".join(bind_flags) or None + xml.put_self_closing("property", **props) else: xml.start_tag("property", **props) self._emit_value(value, xml) xml.end_tag() def _translated_string_attrs( - self, translated: TranslatedStringValue + self, translated: Translated ) -> T.Dict[str, T.Optional[str]]: return { "translatable": "true", - "context": translated.context, + "context": translated.child.context + if isinstance(translated.child, TranslatedWithContext) + else None, } def _emit_signal(self, signal: Signal, xml: XmlEmitter): @@ -154,23 +160,30 @@ class XmlOutput(OutputFormat): xml.end_tag() def _emit_value(self, value: Value, xml: XmlEmitter): - if isinstance(value, IdentValue): - if isinstance(value.parent.value_type, gir.Enumeration): - xml.put_text( - str(value.parent.value_type.members[value.tokens["value"]].value) - ) + if isinstance(value.child, Literal): + literal = value.child.value + if isinstance(literal, IdentLiteral): + value_type = value.context[ValueTypeCtx].value_type + if isinstance(value_type, gir.BoolType): + xml.put_text(literal.ident) + elif isinstance(value_type, gir.Enumeration): + xml.put_text(str(value_type.members[literal.ident].value)) + else: + xml.put_text(literal.ident) + elif isinstance(literal, TypeLiteral): + xml.put_text(literal.type_name.glib_type_name) else: - xml.put_text(value.tokens["value"]) - elif isinstance(value, QuotedValue) or isinstance(value, NumberValue): - xml.put_text(value.value) - elif isinstance(value, FlagsValue): + xml.put_text(literal.value) + elif isinstance(value.child, Flags): xml.put_text( - "|".join([str(flag.value or flag.name) for flag in value.flags]) + "|".join([str(flag.value or flag.name) for flag in value.child.flags]) ) - elif isinstance(value, TranslatedStringValue): + elif isinstance(value.child, Translated): raise CompilerBugError("translated values must be handled in the parent") - elif isinstance(value, TypeValue): - xml.put_text(value.type_name.glib_type_name) + elif isinstance(value.child, TypeLiteral): + xml.put_text(value.child.type_name.glib_type_name) + elif isinstance(value.child, ObjectValue): + self._emit_object(value.child.object, xml) else: raise CompilerBugError() @@ -215,9 +228,9 @@ class XmlOutput(OutputFormat): ): attrs = {attr: name} - if isinstance(value, TranslatedStringValue): - xml.start_tag(tag, **attrs, **self._translated_string_attrs(value)) - xml.put_text(value.string) + if isinstance(value.child, Translated): + xml.start_tag(tag, **attrs, **self._translated_string_attrs(value.child)) + xml.put_text(value.child.child.string) xml.end_tag() else: xml.start_tag(tag, **attrs) @@ -227,43 +240,37 @@ class XmlOutput(OutputFormat): def _emit_extensions(self, extension, xml: XmlEmitter): if isinstance(extension, A11y): xml.start_tag("accessibility") - for child in extension.children: - self._emit_attribute( - child.tag_name, "name", child.name, child.children[Value][0], xml - ) + for prop in extension.properties: + self._emit_attribute(prop.tag_name, "name", prop.name, prop.value, xml) xml.end_tag() elif isinstance(extension, Filters): xml.start_tag(extension.tokens["tag_name"]) - for child in extension.children: - xml.start_tag(child.tokens["tag_name"]) - xml.put_text(child.tokens["name"]) + for prop in extension.children: + xml.start_tag(prop.tokens["tag_name"]) + xml.put_text(prop.tokens["name"]) xml.end_tag() xml.end_tag() elif isinstance(extension, Items): xml.start_tag("items") - for child in extension.children: - self._emit_attribute( - "item", "id", child.name, child.children[Value][0], xml - ) + for prop in extension.children: + self._emit_attribute("item", "id", prop.name, prop.value, xml) xml.end_tag() elif isinstance(extension, Layout): xml.start_tag("layout") - for child in extension.children: - self._emit_attribute( - "property", "name", child.name, child.children[Value][0], xml - ) + for prop in extension.children: + self._emit_attribute("property", "name", prop.name, prop.value, xml) xml.end_tag() elif isinstance(extension, Strings): xml.start_tag("items") - for child in extension.children: - value = child.children[Value][0] - if isinstance(value, TranslatedStringValue): + for prop in extension.children: + value = prop.children[Value][0] + if isinstance(value.child, Translated): xml.start_tag("item", **self._translated_string_attrs(value)) - xml.put_text(value.string) + xml.put_text(value.child.child.string) xml.end_tag() else: xml.start_tag("item") @@ -273,14 +280,14 @@ class XmlOutput(OutputFormat): elif isinstance(extension, Styles): xml.start_tag("style") - for child in extension.children: - xml.put_self_closing("class", name=child.tokens["name"]) + for prop in extension.children: + xml.put_self_closing("class", name=prop.tokens["name"]) xml.end_tag() elif isinstance(extension, Widgets): xml.start_tag("widgets") - for child in extension.children: - xml.put_self_closing("widget", name=child.tokens["name"]) + for prop in extension.children: + xml.put_self_closing("widget", name=prop.tokens["name"]) xml.end_tag() else: diff --git a/blueprintcompiler/parse_tree.py b/blueprintcompiler/parse_tree.py index 670c72e..c85015a 100644 --- a/blueprintcompiler/parse_tree.py +++ b/blueprintcompiler/parse_tree.py @@ -567,6 +567,19 @@ class UseLiteral(ParseNode): return True +class UseExact(ParseNode): + """Matches the given identifier and sets it as a named token.""" + + def __init__(self, key: str, string: str): + self.key = key + self.string = string + + def _parse(self, ctx: ParseContext): + token = ctx.next_token() + ctx.set_group_val(self.key, self.string, token) + return str(token) == self.string + + class Keyword(ParseNode): """Matches the given identifier and sets it as a named token, with the name being the identifier itself.""" diff --git a/blueprintcompiler/parser.py b/blueprintcompiler/parser.py index a44f709..edef840 100644 --- a/blueprintcompiler/parser.py +++ b/blueprintcompiler/parser.py @@ -21,7 +21,7 @@ from .errors import MultipleErrors, PrintableError from .parse_tree import * from .tokenizer import TokenType -from .language import OBJECT_CONTENT_HOOKS, VALUE_HOOKS, Template, UI +from .language import OBJECT_CONTENT_HOOKS, Template, UI def parse( diff --git a/tests/sample_errors/obj_prop_type.err b/tests/sample_errors/obj_prop_type.err index 01f1202..6687c7f 100644 --- a/tests/sample_errors/obj_prop_type.err +++ b/tests/sample_errors/obj_prop_type.err @@ -1 +1 @@ -4,3,21,Cannot assign Gtk.Label to Gtk.Adjustment +4,15,8,Cannot assign Gtk.Label to Gtk.Adjustment diff --git a/tests/samples/binding.blp b/tests/sample_errors/warn_old_bind.blp similarity index 100% rename from tests/samples/binding.blp rename to tests/sample_errors/warn_old_bind.blp diff --git a/tests/sample_errors/warn_old_bind.err b/tests/sample_errors/warn_old_bind.err new file mode 100644 index 0000000..f1acc86 --- /dev/null +++ b/tests/sample_errors/warn_old_bind.err @@ -0,0 +1,2 @@ +4,12,4,Use 'bind-property', introduced in blueprint 0.8.0, to use binding flags +6,12,4,Use 'bind-property', introduced in blueprint 0.8.0, to use binding flags \ No newline at end of file diff --git a/tests/samples/property_binding.blp b/tests/samples/property_binding.blp new file mode 100644 index 0000000..1d7d6ea --- /dev/null +++ b/tests/samples/property_binding.blp @@ -0,0 +1,11 @@ +using Gtk 4.0; + +Box { + visible: bind-property box2.visible inverted; + orientation: bind box2.orientation; + spacing: bind-property box2.spacing no-sync-create; +} + +Box box2 { + spacing: 6; +} diff --git a/tests/samples/binding.ui b/tests/samples/property_binding.ui similarity index 100% rename from tests/samples/binding.ui rename to tests/samples/property_binding.ui diff --git a/tests/samples/property_binding_dec.blp b/tests/samples/property_binding_dec.blp new file mode 100644 index 0000000..7e74ab8 --- /dev/null +++ b/tests/samples/property_binding_dec.blp @@ -0,0 +1,11 @@ +using Gtk 4.0; + +Box { + visible: bind-property box2.visible inverted; + orientation: bind-property box2.orientation; + spacing: bind-property box2.spacing no-sync-create; +} + +Box box2 { + spacing: 6; +} diff --git a/tests/test_samples.py b/tests/test_samples.py index e9e7697..c5caca5 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -145,7 +145,6 @@ class TestSamples(unittest.TestCase): def test_samples(self): self.assert_sample("accessibility") self.assert_sample("action_widgets") - self.assert_sample("binding") self.assert_sample("child_type") self.assert_sample("combo_box_text") self.assert_sample("comments") @@ -163,6 +162,7 @@ class TestSamples(unittest.TestCase): "parseable", skip_run=True ) # The image resource doesn't exist self.assert_sample("property") + self.assert_sample("property_binding") self.assert_sample("signal", skip_run=True) # The callback doesn't exist self.assert_sample("size_group") self.assert_sample("string_list") @@ -235,12 +235,12 @@ class TestSamples(unittest.TestCase): self.assert_sample_error("two_templates") self.assert_sample_error("uint") self.assert_sample_error("using_invalid_namespace") + self.assert_sample_error("warn_old_bind") self.assert_sample_error("warn_old_extern") self.assert_sample_error("widgets_in_non_size_group") def test_decompiler(self): self.assert_decompile("accessibility_dec") - self.assert_decompile("binding") self.assert_decompile("child_type") self.assert_decompile("file_filter") self.assert_decompile("flags") @@ -248,6 +248,7 @@ class TestSamples(unittest.TestCase): self.assert_decompile("layout_dec") self.assert_decompile("menu_dec") self.assert_decompile("property") + self.assert_decompile("property_binding_dec") self.assert_decompile("placeholder_dec") self.assert_decompile("signal") self.assert_decompile("strings")