From bc10ccee0c3ab802f207c08d05d0a6aa37a81af0 Mon Sep 17 00:00:00 2001 From: jgcodes2020 Date: Mon, 23 Dec 2024 20:58:45 -0500 Subject: [PATCH 1/5] format with black --- blueprintcompiler/language/gobject_property.py | 4 +++- blueprintcompiler/language/values.py | 17 +++++++---------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/blueprintcompiler/language/gobject_property.py b/blueprintcompiler/language/gobject_property.py index b267083..d896514 100644 --- a/blueprintcompiler/language/gobject_property.py +++ b/blueprintcompiler/language/gobject_property.py @@ -27,7 +27,9 @@ from .values import ArrayValue, ObjectValue, Value, VariantValue class Property(AstNode): grammar = Statement( - UseIdent("name"), ":", AnyOf(Binding, VariantValue, ObjectValue, Value, ArrayValue) + UseIdent("name"), + ":", + AnyOf(Binding, VariantValue, ObjectValue, Value, ArrayValue), ) @property diff --git a/blueprintcompiler/language/values.py b/blueprintcompiler/language/values.py index 41a4c5e..975dd95 100644 --- a/blueprintcompiler/language/values.py +++ b/blueprintcompiler/language/values.py @@ -371,16 +371,9 @@ class IdentLiteral(AstNode): else: return None + class VariantValue(AstNode): - grammar = [ - "variant", - "<", - UseQuoted("type"), - ">", - "(", - UseQuoted("value"), - ")" - ] + grammar = ["variant", "<", UseQuoted("type"), ">", "(", UseQuoted("value"), ")"] @property def var_type(self) -> str: @@ -404,12 +397,16 @@ class VariantValue(AstNode): raise CompileError(f"Cannot convert variant to number") elif isinstance(expected_type, gir.StringType): raise CompileError("Cannot convert variant to string") - elif isinstance(expected_type, gir.Boxed) and expected_type.full_name == "GLib.Variant": + elif ( + isinstance(expected_type, gir.Boxed) + and expected_type.full_name == "GLib.Variant" + ): pass else: raise CompileError(f"Cannot convert variant into {expected_type.full_name}") pass + class Literal(AstNode): grammar = AnyOf( TypeLiteral, From be667a2b9c8412a74d6909f37390e48b0ca14e49 Mon Sep 17 00:00:00 2001 From: jgcodes2020 Date: Tue, 24 Dec 2024 08:50:37 -0500 Subject: [PATCH 2/5] implement variant parsing --- blueprintcompiler/language/gtk_menu.py | 5 +- blueprintcompiler/language/values.py | 5 +- blueprintcompiler/language/variant.py | 119 +++++++++++++++++++++++++ variant-test.blp | 26 +++--- 4 files changed, 139 insertions(+), 16 deletions(-) create mode 100644 blueprintcompiler/language/variant.py diff --git a/blueprintcompiler/language/gtk_menu.py b/blueprintcompiler/language/gtk_menu.py index 9e76d61..a703a76 100644 --- a/blueprintcompiler/language/gtk_menu.py +++ b/blueprintcompiler/language/gtk_menu.py @@ -137,7 +137,10 @@ menu_attribute = Group( [ UseIdent("name"), ":", - Err(AnyOf(StringValue, VariantValue), "Expected string or translated string"), + Err( + AnyOf(StringValue, VariantValue), + "Expected string, translated string, or variant", + ), Match(";").expected(), ], ) diff --git a/blueprintcompiler/language/values.py b/blueprintcompiler/language/values.py index 975dd95..4c7aab9 100644 --- a/blueprintcompiler/language/values.py +++ b/blueprintcompiler/language/values.py @@ -26,6 +26,7 @@ from .common import * from .contexts import ScopeCtx, ValueTypeCtx from .gobject_object import Object from .types import TypeName +from .variant import VarContent class Translated(AstNode): @@ -373,7 +374,7 @@ class IdentLiteral(AstNode): class VariantValue(AstNode): - grammar = ["variant", "<", UseQuoted("type"), ">", "(", UseQuoted("value"), ")"] + grammar = ["variant", "<", UseQuoted("type"), ">", "(", VarContent, ")"] @property def var_type(self) -> str: @@ -381,7 +382,7 @@ class VariantValue(AstNode): @property def var_value(self) -> str: - return self.tokens["value"] + return self.children[0].content @validate() def validate_for_type(self) -> None: diff --git a/blueprintcompiler/language/variant.py b/blueprintcompiler/language/variant.py new file mode 100644 index 0000000..62c8458 --- /dev/null +++ b/blueprintcompiler/language/variant.py @@ -0,0 +1,119 @@ +import typing as T + +from blueprintcompiler.gir import ArrayType +from blueprintcompiler.lsp_utils import SemanticToken + +from .common import * +from .contexts import ScopeCtx, ValueTypeCtx +from .gobject_object import Object +from .types import TypeName + +VAR_CONTENT_HOOKS = [] + + +class VarContent(AstNode): + grammar = AnyOf(*VAR_CONTENT_HOOKS) + + @property + def content(self) -> str: + return self.children[0].content + + +class VarContentBool(AstNode): + grammar = AnyOf( + [Keyword("true"), UseLiteral("value", True)], + [Keyword("false"), UseLiteral("value", False)], + ) + + @property + def content(self) -> str: + if self.tokens["value"]: + return "true" + else: + return "false" + + +class VarContentString(AstNode): + grammar = UseQuoted("value") + + @property + def content(self) -> str: + return self.tokens["value"] + + +class VarContentNumber(AstNode): + grammar = UseNumberText("value") + + @property + def content(self) -> str: + return self.tokens["value"] + + +class VarContentTuple(AstNode): + grammar = ["(", Delimited(VarContent, ","), ")"] + + @property + def content(self) -> str: + inner = ", ".join(child.content for child in self.children) + return f"({inner})" + + +class VarContentArray(AstNode): + grammar = ["[", Delimited(VarContent, ","), "]"] + + @property + def content(self) -> str: + inner = ", ".join(child.content for child in self.children) + return f"[{inner}]" + + +class VarContentDictEntry(AstNode): + grammar = ["{", VarContent, ",", VarContent, "}"] + + @property + def content(self): + return f"{{{self.children[0].content}, {self.children[1].content}}}" + + +class VarContentDict(AstNode): + grammar = ["{", Delimited([VarContent, ":", VarContent], ","), "}"] + + @property + def content(self) -> str: + inner = ", ".join(child.content for child in self.children) + return f"{{{inner}}}" + + +class VarContentVariant(AstNode): + grammar = ["<", VarContent, ">"] + + @property + def content(self) -> str: + return f"<{self.children[0].content}>" + + +class VarContentMaybe(AstNode): + grammar = AnyOf( + [Keyword("just"), VarContent], + [Keyword("nothing")], + ) + + @property + def content(self) -> str: + if self.children[0] is not None: + return f"just {self.children[0].content}" + else: + return "nothing" + + +VarContent.grammar.children = [ + VarContentString, + VarContentNumber, + VarContentBool, + VarContentMaybe, + VarContentTuple, + VarContentDict, + VarContentDictEntry, + VarContentArray, + VarContentVariant, +] diff --git a/variant-test.blp b/variant-test.blp index f69522f..8cdca87 100644 --- a/variant-test.blp +++ b/variant-test.blp @@ -1,16 +1,16 @@ using Gtk 4.0; -menu root { - submenu { - name: "one"; - item { - action: "app.foo_bar"; - target: variant<"s">("\"one\""); - } - } -} - -Button { - action-name: "app.shave_yak"; - action-target: variant<"y">("8"); +$BlueprintTestObject { + // test-one: variant<"s">("one"); + test-zero: variant<"b">(true); + test-one: variant<"s">("one"); + test-two: variant<"i">(2); + test-three: variant<"(ii)">((3, 4)); + test-four: variant<"ai">([5, 6]); + test-five: variant<"{sv}">({"key", <"value">}); + test-six: variant<"a{ss}">({ + "GLib": "2.24", + "Gtk": "4.16" + }); + test-seven: variant<"ams">([just "2", nothing]); } \ No newline at end of file From 96c5760eafecb538e7fc6c698a65bbd97e7fbc4e Mon Sep 17 00:00:00 2001 From: jgcodes2020 Date: Tue, 24 Dec 2024 09:49:53 -0500 Subject: [PATCH 3/5] debug variant parsing --- blueprintcompiler/language/values.py | 33 ++++++++++++++++++++++++++- blueprintcompiler/language/variant.py | 7 ++++-- blueprintcompiler/utils.py | 15 ++++++++++++ 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/blueprintcompiler/language/values.py b/blueprintcompiler/language/values.py index 4c7aab9..4d355c9 100644 --- a/blueprintcompiler/language/values.py +++ b/blueprintcompiler/language/values.py @@ -28,6 +28,10 @@ from .gobject_object import Object from .types import TypeName from .variant import VarContent +import gi +gi.require_version("GLib", "2.0") +from gi.repository import GLib + class Translated(AstNode): grammar = AnyOf( @@ -373,8 +377,17 @@ class IdentLiteral(AstNode): return None + class VariantValue(AstNode): - grammar = ["variant", "<", UseQuoted("type"), ">", "(", VarContent, ")"] + grammar = [ + "variant", + "<", + UseQuoted("type"), + ">", + "(", + Err(VarContent, "Invalid variant content!"), + ")", + ] @property def var_type(self) -> str: @@ -407,6 +420,24 @@ class VariantValue(AstNode): raise CompileError(f"Cannot convert variant into {expected_type.full_name}") pass + @validate("type") + def validate_type(self): + if not GLib.VariantType.string_is_valid(self.var_type): + raise CompileError(f"`{self.var_type}` is not a valid variant type") + + @validate() + def validate_content(self): + if not GLib.VariantType.string_is_valid(self.var_type): + return + + try: + var_ty = GLib.VariantType.new(self.var_type) + var_val = GLib.Variant.parse(var_ty, self.var_value) + except GLib.GError as error: + raise CompileError(f"Variant did not match specified type: {error}") + + pass + class Literal(AstNode): grammar = AnyOf( diff --git a/blueprintcompiler/language/variant.py b/blueprintcompiler/language/variant.py index 62c8458..bc0988f 100644 --- a/blueprintcompiler/language/variant.py +++ b/blueprintcompiler/language/variant.py @@ -38,7 +38,7 @@ class VarContentString(AstNode): @property def content(self) -> str: - return self.tokens["value"] + return utils.escape_quote(self.tokens["value"]) class VarContentNumber(AstNode): @@ -80,7 +80,10 @@ class VarContentDict(AstNode): @property def content(self) -> str: - inner = ", ".join(child.content for child in self.children) + inner = ", ".join( + f"{key.content}: {value.content}" + for (key, value) in utils.iter_batched(self.children, 2, strict=True) + ) return f"{{{inner}}}" diff --git a/blueprintcompiler/utils.py b/blueprintcompiler/utils.py index ea8102e..f579ea4 100644 --- a/blueprintcompiler/utils.py +++ b/blueprintcompiler/utils.py @@ -19,6 +19,7 @@ import typing as T from dataclasses import dataclass +import itertools class Colors: @@ -154,3 +155,17 @@ def unescape_quote(string: str) -> str: i += 1 return result + +def iter_batched(iterable, n, *, strict=False): + """ + Replacement for `itertools.batched()` since the testing infrastructure + uses Python 3.9 at the moment. Copied directly off of the Python docs. + """ + # batched('ABCDEFG', 3) → ABC DEF G + if n < 1: + raise ValueError('n must be at least one') + iterator = iter(iterable) + while batch := tuple(itertools.islice(iterator, n)): + if strict and len(batch) != n: + raise ValueError('batched(): incomplete batch') + yield batch \ No newline at end of file From c708532a8d863836e045faf5d246bdbf8543921d Mon Sep 17 00:00:00 2001 From: jgcodes2020 Date: Tue, 24 Dec 2024 10:37:02 -0500 Subject: [PATCH 4/5] move variants into testing --- tests/samples/variants.blp | 42 ++++++++++++++++++++++++++++++++++++ tests/samples/variants.ui | 44 ++++++++++++++++++++++++++++++++++++++ variant-test.blp | 16 -------------- 3 files changed, 86 insertions(+), 16 deletions(-) create mode 100644 tests/samples/variants.blp create mode 100644 tests/samples/variants.ui delete mode 100644 variant-test.blp diff --git a/tests/samples/variants.blp b/tests/samples/variants.blp new file mode 100644 index 0000000..a45069a --- /dev/null +++ b/tests/samples/variants.blp @@ -0,0 +1,42 @@ +using Gtk 4.0; + +$BlueprintTestObject { + // test-one: variant<"s">("one"); + test-zero: variant<"b">(true); + test-one: variant<"s">("one"); + test-two: variant<"i">(2); + test-three: variant<"(ii)">((3, 4)); + test-four: variant<"ai">([5, 6]); + test-five: variant<"{sv}">({"key", <"value">}); + test-six: variant<"a{ss}">({ + "GLib": "2.24", + "Gtk": "4.16" + }); + test-seven: variant<"ams">([just "2", nothing]); +} + +menu test_menu { + submenu { + label: "Test menu"; + item { + label: "Option 1"; + action: "app.test_menu.set_action"; + target: variant<"y">(1); + } + item { + label: "Option 2"; + action: "app.test_menu.set_action"; + target: variant<"y">(2); + } + item { + label: "Option 3"; + action: "app.test_menu.set_action"; + target: variant<"y">(3); + } + item { + label: "Option 4"; + action: "app.test_menu.set_action"; + target: variant<"y">(4); + } + } +} \ No newline at end of file diff --git a/tests/samples/variants.ui b/tests/samples/variants.ui new file mode 100644 index 0000000..4fa0a26 --- /dev/null +++ b/tests/samples/variants.ui @@ -0,0 +1,44 @@ + + + + + + true + "one" + 2 + (3, 4) + [5, 6] + {"key", <"value">} + {"GLib": "2.24", "Gtk": "4.16"} + [just "2", nothing] + + + + Test menu + + Option 1 + app.test_menu.set_action + 1 + + + Option 2 + app.test_menu.set_action + 2 + + + Option 3 + app.test_menu.set_action + 3 + + + Option 4 + app.test_menu.set_action + 4 + + + + \ No newline at end of file diff --git a/variant-test.blp b/variant-test.blp deleted file mode 100644 index 8cdca87..0000000 --- a/variant-test.blp +++ /dev/null @@ -1,16 +0,0 @@ -using Gtk 4.0; - -$BlueprintTestObject { - // test-one: variant<"s">("one"); - test-zero: variant<"b">(true); - test-one: variant<"s">("one"); - test-two: variant<"i">(2); - test-three: variant<"(ii)">((3, 4)); - test-four: variant<"ai">([5, 6]); - test-five: variant<"{sv}">({"key", <"value">}); - test-six: variant<"a{ss}">({ - "GLib": "2.24", - "Gtk": "4.16" - }); - test-seven: variant<"ams">([just "2", nothing]); -} \ No newline at end of file From ab9d902cc5e63ee503a627c8406240c86a4556f9 Mon Sep 17 00:00:00 2001 From: jgcodes2020 Date: Tue, 24 Dec 2024 10:41:35 -0500 Subject: [PATCH 5/5] fix formatting and some tests --- blueprintcompiler/language/gtk_menu.py | 2 +- blueprintcompiler/language/values.py | 4 ++-- blueprintcompiler/language/variant.py | 2 +- blueprintcompiler/utils.py | 7 ++++--- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/blueprintcompiler/language/gtk_menu.py b/blueprintcompiler/language/gtk_menu.py index a703a76..5a6627a 100644 --- a/blueprintcompiler/language/gtk_menu.py +++ b/blueprintcompiler/language/gtk_menu.py @@ -98,7 +98,7 @@ class MenuAttribute(AstNode): return self.tokens["name"] @property - def value(self) -> StringValue | VariantValue: + def value(self) -> T.Union[StringValue, VariantValue]: if len(self.children[StringValue]) > 0: return self.children[StringValue][0] elif len(self.children[VariantValue]) > 0: diff --git a/blueprintcompiler/language/values.py b/blueprintcompiler/language/values.py index 4d355c9..021cf90 100644 --- a/blueprintcompiler/language/values.py +++ b/blueprintcompiler/language/values.py @@ -29,6 +29,7 @@ from .types import TypeName from .variant import VarContent import gi + gi.require_version("GLib", "2.0") from gi.repository import GLib @@ -377,7 +378,6 @@ class IdentLiteral(AstNode): return None - class VariantValue(AstNode): grammar = [ "variant", @@ -424,7 +424,7 @@ class VariantValue(AstNode): def validate_type(self): if not GLib.VariantType.string_is_valid(self.var_type): raise CompileError(f"`{self.var_type}` is not a valid variant type") - + @validate() def validate_content(self): if not GLib.VariantType.string_is_valid(self.var_type): diff --git a/blueprintcompiler/language/variant.py b/blueprintcompiler/language/variant.py index bc0988f..450a0c8 100644 --- a/blueprintcompiler/language/variant.py +++ b/blueprintcompiler/language/variant.py @@ -8,7 +8,7 @@ from .contexts import ScopeCtx, ValueTypeCtx from .gobject_object import Object from .types import TypeName -VAR_CONTENT_HOOKS = [] +VAR_CONTENT_HOOKS: list[T.Any] = [] class VarContent(AstNode): diff --git a/blueprintcompiler/utils.py b/blueprintcompiler/utils.py index f579ea4..b05c9cd 100644 --- a/blueprintcompiler/utils.py +++ b/blueprintcompiler/utils.py @@ -156,6 +156,7 @@ def unescape_quote(string: str) -> str: return result + def iter_batched(iterable, n, *, strict=False): """ Replacement for `itertools.batched()` since the testing infrastructure @@ -163,9 +164,9 @@ def iter_batched(iterable, n, *, strict=False): """ # batched('ABCDEFG', 3) → ABC DEF G if n < 1: - raise ValueError('n must be at least one') + raise ValueError("n must be at least one") iterator = iter(iterable) while batch := tuple(itertools.islice(iterator, n)): if strict and len(batch) != n: - raise ValueError('batched(): incomplete batch') - yield batch \ No newline at end of file + raise ValueError("batched(): incomplete batch") + yield batch