diff --git a/blueprintcompiler/ast_utils.py b/blueprintcompiler/ast_utils.py index 5019098..7bfacfb 100644 --- a/blueprintcompiler/ast_utils.py +++ b/blueprintcompiler/ast_utils.py @@ -109,9 +109,9 @@ class AstNode: else: return self.parent.root - def parent_by_type(self, type): + def parent_by_type(self, type: T.Type[TType]) -> TType: if self.parent is None: - return None + raise CompilerBugError() elif isinstance(self.parent, type): return self.parent else: diff --git a/blueprintcompiler/language/__init__.py b/blueprintcompiler/language/__init__.py index 45e5361..571f09b 100644 --- a/blueprintcompiler/language/__init__.py +++ b/blueprintcompiler/language/__init__.py @@ -23,7 +23,7 @@ from .gtk_menu import menu, Menu, MenuAttribute from .gtk_size_group import Widgets from .gtk_string_list import Strings from .gtk_styles import Styles -from .gtkbuilder_child import Child +from .gtkbuilder_child import Child, ChildType, ChildInternal, ChildExtension from .gtkbuilder_template import Template from .imports import GtkDirective, Import from .property_binding import PropertyBinding diff --git a/blueprintcompiler/language/gobject_object.py b/blueprintcompiler/language/gobject_object.py index a66ffe6..1dbbce5 100644 --- a/blueprintcompiler/language/gobject_object.py +++ b/blueprintcompiler/language/gobject_object.py @@ -22,7 +22,7 @@ import typing as T from functools import cached_property from .common import * -from .response_id import ResponseId +from .response_id import ExtResponse from .types import ClassName, ConcreteClassName @@ -60,7 +60,7 @@ class Object(AstNode): return self.class_name.gir_type @cached_property - def action_widgets(self) -> T.List[ResponseId]: + def action_widgets(self) -> T.List[ExtResponse]: """Get list of widget's action widgets. Empty if object doesn't have action widgets. @@ -69,7 +69,7 @@ class Object(AstNode): return [ child.response_id - for child in self.children[ObjectContent][0].children[Child] + for child in self.content.children[Child] if child.response_id ] diff --git a/blueprintcompiler/language/gtkbuilder_child.py b/blueprintcompiler/language/gtkbuilder_child.py index 45b2074..c3e1c47 100644 --- a/blueprintcompiler/language/gtkbuilder_child.py +++ b/blueprintcompiler/language/gtkbuilder_child.py @@ -21,7 +21,7 @@ from functools import cached_property from .gobject_object import Object -from .response_id import ResponseId +from .response_id import ExtResponse from .common import * ALLOWED_PARENTS: T.List[T.Tuple[str, str]] = [ @@ -30,20 +30,49 @@ ALLOWED_PARENTS: T.List[T.Tuple[str, str]] = [ ] +class ChildInternal(AstNode): + grammar = ["internal-child", UseIdent("internal_child")] + + @property + def internal_child(self) -> str: + return self.tokens["internal_child"] + + +class ChildType(AstNode): + grammar = UseIdent("child_type").expected("a child type") + + @property + def child_type(self) -> str: + return self.tokens["child_type"] + + +class ChildExtension(AstNode): + grammar = ExtResponse + + @property + def child(self) -> ExtResponse: + return self.children[0] + + +class ChildAnnotation(AstNode): + grammar = ["[", AnyOf(ChildInternal, ChildExtension, ChildType), "]"] + + @property + def child(self) -> T.Union[ChildInternal, ChildExtension, ChildType]: + return self.children[0] + + class Child(AstNode): grammar = [ - Optional( - [ - "[", - Optional(["internal-child", UseLiteral("internal_child", True)]), - UseIdent("child_type").expected("a child type"), - Optional(ResponseId), - "]", - ] - ), + Optional(ChildAnnotation), Object, ] + @property + def annotation(self) -> T.Optional[ChildAnnotation]: + annotations = self.children[ChildAnnotation] + return annotations[0] if len(annotations) else None + @property def object(self) -> Object: return self.children[Object][0] @@ -69,15 +98,17 @@ class Child(AstNode): ) @cached_property - def response_id(self) -> T.Optional[ResponseId]: + def response_id(self) -> T.Optional[ExtResponse]: """Get action widget's response ID. If child is not action widget, returns `None`. """ - response_ids = self.children[ResponseId] - - if response_ids: - return response_ids[0] + if ( + self.annotation is not None + and isinstance(self.annotation.child, ChildExtension) + and isinstance(self.annotation.child.child, ExtResponse) + ): + return self.annotation.child.child else: return None diff --git a/blueprintcompiler/language/response_id.py b/blueprintcompiler/language/response_id.py index e3b44a9..7de197c 100644 --- a/blueprintcompiler/language/response_id.py +++ b/blueprintcompiler/language/response_id.py @@ -23,12 +23,13 @@ import typing as T from .common import * -class ResponseId(AstNode): +class ExtResponse(AstNode): """Response ID of action widget.""" ALLOWED_PARENTS: T.List[T.Tuple[str, str]] = [("Gtk", "Dialog"), ("Gtk", "InfoBar")] grammar = [ + Keyword("action"), Keyword("response"), "=", AnyOf( @@ -41,13 +42,6 @@ class ResponseId(AstNode): Optional([Keyword("default"), UseLiteral("is_default", True)]), ] - @validate() - def child_type_is_action(self) -> None: - """Check that child type is "action".""" - child_type = self.parent.tokens["child_type"] - if child_type != "action": - raise CompileError(f"Only action widget can have response ID") - @validate() def parent_has_action_widgets(self) -> None: """Chech that parent widget has allowed type.""" @@ -59,7 +53,7 @@ class ResponseId(AstNode): gir = self.root.gir - for namespace, name in ResponseId.ALLOWED_PARENTS: + for namespace, name in ExtResponse.ALLOWED_PARENTS: parent_type = gir.get_type(name, namespace) if container_type.assignable_to(parent_type): break @@ -71,10 +65,10 @@ class ResponseId(AstNode): @validate() def widget_have_id(self) -> None: """Check that action widget have ID.""" - from .gobject_object import Object + from .gtkbuilder_child import Child - _object = self.parent.children[Object][0] - if _object.tokens["id"] is None: + object = self.parent_by_type(Child).object + if object.id is None: raise CompileError(f"Action widget must have ID") @validate("response_id") @@ -102,10 +96,9 @@ class ResponseId(AstNode): @validate("default") def no_multiple_default(self) -> None: """Only one action widget in dialog can be default.""" - from .gtkbuilder_child import Child from .gobject_object import Object - if not self.tokens["is_default"]: + if not self.is_default: return action_widgets = self.parent_by_type(Object).action_widgets @@ -126,7 +119,7 @@ class ResponseId(AstNode): @property def widget_id(self) -> str: """Get action widget ID.""" - from .gobject_object import Object + from .gtkbuilder_child import Child - _object: Object = self.parent.children[Object][0] - return _object.tokens["id"] + object = self.parent_by_type(Child).object + return object.id diff --git a/blueprintcompiler/outputs/xml/__init__.py b/blueprintcompiler/outputs/xml/__init__.py index 607cc36..8332cee 100644 --- a/blueprintcompiler/outputs/xml/__init__.py +++ b/blueprintcompiler/outputs/xml/__init__.py @@ -170,11 +170,16 @@ class XmlOutput(OutputFormat): def _emit_child(self, child: Child, xml: XmlEmitter): child_type = internal_child = None - - if child.tokens["internal_child"]: - internal_child = child.tokens["child_type"] - else: - child_type = child.tokens["child_type"] + if child.annotation is not None: + annotation = child.annotation.child + if isinstance(annotation, ChildType): + child_type = annotation.child_type + elif isinstance(annotation, ChildInternal): + internal_child = annotation.internal_child + elif isinstance(annotation, ChildExtension): + child_type = "action" + else: + raise CompilerBugError() xml.start_tag("child", type=child_type, internal_child=internal_child) self._emit_object(child.object, xml) diff --git a/tests/sample_errors/action_widget_have_no_id.err b/tests/sample_errors/action_widget_have_no_id.err index 74ff665..b239d77 100644 --- a/tests/sample_errors/action_widget_have_no_id.err +++ b/tests/sample_errors/action_widget_have_no_id.err @@ -1 +1 @@ -4,13,15,Action widget must have ID +4,6,22,Action widget must have ID diff --git a/tests/sample_errors/action_widget_in_invalid_container.err b/tests/sample_errors/action_widget_in_invalid_container.err index 20950a1..ef3296c 100644 --- a/tests/sample_errors/action_widget_in_invalid_container.err +++ b/tests/sample_errors/action_widget_in_invalid_container.err @@ -1 +1 @@ -4,13,11,Gtk.Box doesn't have action widgets +4,6,18,Gtk.Box doesn't have action widgets diff --git a/tests/sample_errors/action_widget_not_action.blp b/tests/sample_errors/action_widget_not_action.blp deleted file mode 100644 index cd760e5..0000000 --- a/tests/sample_errors/action_widget_not_action.blp +++ /dev/null @@ -1,8 +0,0 @@ -using Gtk 4.0; - -Dialog { - [some_type response=ok] - Button ok_button { - - } -} diff --git a/tests/sample_errors/action_widget_not_action.err b/tests/sample_errors/action_widget_not_action.err deleted file mode 100644 index 95699ff..0000000 --- a/tests/sample_errors/action_widget_not_action.err +++ /dev/null @@ -1 +0,0 @@ -4,16,11,Only action widget can have response ID diff --git a/tests/test_samples.py b/tests/test_samples.py index 7af9e1b..88cecdf 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -216,7 +216,6 @@ class TestSamples(unittest.TestCase): self.assert_sample_error("action_widget_float_response") self.assert_sample_error("action_widget_have_no_id") self.assert_sample_error("action_widget_multiple_default") - self.assert_sample_error("action_widget_not_action") self.assert_sample_error("action_widget_in_invalid_container") self.assert_sample_error("action_widget_response_dne") self.assert_sample_error("action_widget_negative_response")