From 8fee46ec686ca4232feffea6c6d1b51daff4f4c0 Mon Sep 17 00:00:00 2001 From: James Westman Date: Mon, 19 Dec 2022 11:49:10 -0600 Subject: [PATCH] Format using black --- blueprintcompiler/ast_utils.py | 28 ++- blueprintcompiler/completions.py | 49 +++-- blueprintcompiler/completions_utils.py | 15 +- blueprintcompiler/decompiler.py | 103 +++++++--- blueprintcompiler/errors.py | 51 +++-- blueprintcompiler/gir.py | 127 ++++++++---- blueprintcompiler/interactive_port.py | 107 +++++++--- blueprintcompiler/language/__init__.py | 11 +- blueprintcompiler/language/attributes.py | 6 +- blueprintcompiler/language/gobject_object.py | 9 +- .../language/gobject_property.py | 41 ++-- blueprintcompiler/language/gobject_signal.py | 31 ++- blueprintcompiler/language/gtk_a11y.py | 13 +- .../language/gtk_combo_box_text.py | 17 +- blueprintcompiler/language/gtk_file_filter.py | 18 +- blueprintcompiler/language/gtk_layout.py | 7 +- blueprintcompiler/language/gtk_menu.py | 120 +++++------- blueprintcompiler/language/gtk_size_group.py | 2 +- blueprintcompiler/language/gtk_string_list.py | 5 +- blueprintcompiler/language/gtk_styles.py | 3 +- .../language/gtkbuilder_child.py | 27 ++- .../language/gtkbuilder_template.py | 14 +- blueprintcompiler/language/imports.py | 13 +- blueprintcompiler/language/response_id.py | 24 +-- blueprintcompiler/language/types.py | 15 +- blueprintcompiler/language/ui.py | 28 +-- blueprintcompiler/language/values.py | 43 ++-- blueprintcompiler/lsp.py | 185 +++++++++++------- blueprintcompiler/lsp_utils.py | 10 +- blueprintcompiler/main.py | 43 ++-- blueprintcompiler/outputs/__init__.py | 2 + blueprintcompiler/parse_tree.py | 182 ++++++++++------- blueprintcompiler/parser.py | 2 +- blueprintcompiler/tokenizer.py | 48 ++--- blueprintcompiler/typelib.py | 4 +- blueprintcompiler/utils.py | 30 +-- blueprintcompiler/xml_reader.py | 31 +-- tests/fuzz.py | 9 +- tests/test_samples.py | 30 +-- tests/test_tokenizer.py | 82 ++++---- 40 files changed, 975 insertions(+), 610 deletions(-) diff --git a/blueprintcompiler/ast_utils.py b/blueprintcompiler/ast_utils.py index cc5c44d..e4f2efa 100644 --- a/blueprintcompiler/ast_utils.py +++ b/blueprintcompiler/ast_utils.py @@ -26,11 +26,14 @@ from .lsp_utils import SemanticToken class Children: - """ Allows accessing children by type using array syntax. """ + """Allows accessing children by type using array syntax.""" + def __init__(self, children): self._children = children + def __iter__(self): return iter(self._children) + def __getitem__(self, key): if isinstance(key, int): return self._children[key] @@ -39,7 +42,7 @@ class Children: class AstNode: - """ Base class for nodes in the abstract syntax tree. """ + """Base class for nodes in the abstract syntax tree.""" completers: T.List = [] @@ -55,8 +58,9 @@ class AstNode: def __init_subclass__(cls): cls.completers = [] - cls.validators = [getattr(cls, f) for f in dir(cls) if hasattr(getattr(cls, f), "_validator")] - + cls.validators = [ + getattr(cls, f) for f in dir(cls) if hasattr(getattr(cls, f), "_validator") + ] @property def root(self): @@ -116,13 +120,11 @@ class AstNode: for child in self.children: yield from child.get_semantic_tokens() - def iterate_children_recursive(self) -> T.Iterator["AstNode"]: yield self for child in self.children: yield from child.iterate_children_recursive() - def validate_unique_in_parent(self, error, check=None): for child in self.parent.children: if child is self: @@ -132,13 +134,19 @@ class AstNode: if check is None or check(child): raise CompileError( error, - references=[ErrorReference(child.group.start, child.group.end, "previous declaration was here")] + references=[ + ErrorReference( + child.group.start, + child.group.end, + "previous declaration was here", + ) + ], ) def validate(token_name=None, end_token_name=None, skip_incomplete=False): - """ Decorator for functions that validate an AST node. Exceptions raised - during validation are marked with range information from the tokens. """ + """Decorator for functions that validate an AST node. Exceptions raised + during validation are marked with range information from the tokens.""" def decorator(func): def inner(self): @@ -191,7 +199,7 @@ class Docs: def docs(*args, **kwargs): - """ Decorator for functions that return documentation for tokens. """ + """Decorator for functions that return documentation for tokens.""" def decorator(func): return Docs(func, *args, **kwargs) diff --git a/blueprintcompiler/completions.py b/blueprintcompiler/completions.py index 7566f18..085baee 100644 --- a/blueprintcompiler/completions.py +++ b/blueprintcompiler/completions.py @@ -30,9 +30,13 @@ from .tokenizer import TokenType, Token Pattern = T.List[T.Tuple[TokenType, T.Optional[str]]] -def _complete(ast_node: AstNode, tokens: T.List[Token], idx: int, token_idx: int) -> T.Iterator[Completion]: +def _complete( + ast_node: AstNode, tokens: T.List[Token], idx: int, token_idx: int +) -> T.Iterator[Completion]: for child in ast_node.children: - if child.group.start <= idx and (idx < child.group.end or (idx == child.group.end and child.incomplete)): + if child.group.start <= idx and ( + idx < child.group.end or (idx == child.group.end and child.incomplete) + ): yield from _complete(child, tokens, idx, token_idx) return @@ -49,7 +53,9 @@ def _complete(ast_node: AstNode, tokens: T.List[Token], idx: int, token_idx: int yield from completer(prev_tokens, ast_node) -def complete(ast_node: AstNode, tokens: T.List[Token], idx: int) -> T.Iterator[Completion]: +def complete( + ast_node: AstNode, tokens: T.List[Token], idx: int +) -> T.Iterator[Completion]: token_idx = 0 # find the current token for i, token in enumerate(tokens): @@ -71,13 +77,17 @@ def using_gtk(ast_node, match_variables): @completer( applies_in=[language.UI, language.ObjectContent, language.Template], - matches=new_statement_patterns + matches=new_statement_patterns, ) def namespace(ast_node, match_variables): yield Completion("Gtk", CompletionItemKind.Module, text="Gtk.") for ns in ast_node.root.children[language.Import]: if ns.gir_namespace is not None: - yield Completion(ns.gir_namespace.name, CompletionItemKind.Module, text=ns.gir_namespace.name + ".") + yield Completion( + ns.gir_namespace.name, + CompletionItemKind.Module, + text=ns.gir_namespace.name + ".", + ) @completer( @@ -85,7 +95,7 @@ def namespace(ast_node, match_variables): matches=[ [(TokenType.IDENT, None), (TokenType.OP, "."), (TokenType.IDENT, None)], [(TokenType.IDENT, None), (TokenType.OP, ".")], - ] + ], ) def object_completer(ast_node, match_variables): ns = ast_node.root.gir.namespaces.get(match_variables[0]) @@ -117,9 +127,7 @@ def property_completer(ast_node, match_variables): @completer( applies_in=[language.Property, language.BaseTypedAttribute], - matches=[ - [(TokenType.IDENT, None), (TokenType.OP, ":")] - ], + matches=[[(TokenType.IDENT, None), (TokenType.OP, ":")]], ) def prop_value_completer(ast_node, match_variables): if isinstance(ast_node.value_type, gir.Enumeration): @@ -141,16 +149,23 @@ def signal_completer(ast_node, match_variables): if not isinstance(ast_node.parent, language.Object): name = "on" else: - name = "on_" + (ast_node.parent.children[ClassName][0].tokens["id"] or ast_node.parent.children[ClassName][0].tokens["class_name"].lower()) - yield Completion(signal, CompletionItemKind.Property, snippet=f"{signal} => ${{1:{name}_{signal.replace('-', '_')}}}()$0;") + name = "on_" + ( + ast_node.parent.children[ClassName][0].tokens["id"] + or ast_node.parent.children[ClassName][0] + .tokens["class_name"] + .lower() + ) + yield Completion( + signal, + CompletionItemKind.Property, + snippet=f"{signal} => ${{1:{name}_{signal.replace('-', '_')}}}()$0;", + ) -@completer( - applies_in=[language.UI], - matches=new_statement_patterns -) +@completer(applies_in=[language.UI], matches=new_statement_patterns) def template_completer(ast_node, match_variables): yield Completion( - "template", CompletionItemKind.Snippet, - snippet="template ${1:ClassName} : ${2:ParentClass} {\n $0\n}" + "template", + CompletionItemKind.Snippet, + snippet="template ${1:ClassName} : ${2:ParentClass} {\n $0\n}", ) diff --git a/blueprintcompiler/completions_utils.py b/blueprintcompiler/completions_utils.py index 85f5159..b9811b9 100644 --- a/blueprintcompiler/completions_utils.py +++ b/blueprintcompiler/completions_utils.py @@ -32,20 +32,25 @@ new_statement_patterns = [ def applies_to(*ast_types): - """ Decorator describing which AST nodes the completer should apply in. """ + """Decorator describing which AST nodes the completer should apply in.""" + def decorator(func): for c in ast_types: c.completers.append(func) return func + return decorator -def completer(applies_in: T.List, matches: T.List=[], applies_in_subclass=None): + +def completer(applies_in: T.List, matches: T.List = [], applies_in_subclass=None): def decorator(func): def inner(prev_tokens: T.List[Token], ast_node): # For completers that apply in ObjectContent nodes, we can further # check that the object is the right class if applies_in_subclass is not None: - type = ast_node.root.gir.get_type(applies_in_subclass[1], applies_in_subclass[0]) + type = ast_node.root.gir.get_type( + applies_in_subclass[1], applies_in_subclass[0] + ) if ast_node.gir_class and not ast_node.gir_class.assignable_to(type): return @@ -59,7 +64,9 @@ def completer(applies_in: T.List, matches: T.List=[], applies_in_subclass=None): for i in range(0, len(pattern)): type, value = pattern[i] token = prev_tokens[i - len(pattern)] - if token.type != type or (value is not None and str(token) != value): + if token.type != type or ( + value is not None and str(token) != value + ): break if value is None: match_variables.append(str(token)) diff --git a/blueprintcompiler/decompiler.py b/blueprintcompiler/decompiler.py index cd66386..145e4be 100644 --- a/blueprintcompiler/decompiler.py +++ b/blueprintcompiler/decompiler.py @@ -60,16 +60,16 @@ class DecompileCtx: self.gir.add_namespace(get_namespace("Gtk", "4.0")) - @property def result(self): - imports = "\n".join([ - f"using {ns} {namespace.version};" - for ns, namespace in self.gir.namespaces.items() - ]) + imports = "\n".join( + [ + f"using {ns} {namespace.version};" + for ns, namespace in self.gir.namespaces.items() + ] + ) return imports + "\n" + self._result - def type_by_cname(self, cname): if type := self.gir.get_type_by_cname(cname): return type @@ -83,7 +83,6 @@ class DecompileCtx: except: pass - def start_block(self): self._blocks_need_end.append(None) @@ -94,7 +93,6 @@ class DecompileCtx: def end_block_with(self, text): self._blocks_need_end[-1] = text - def print(self, line, newline=True): if line == "}" or line == "]": self._indent -= 1 @@ -109,7 +107,11 @@ class DecompileCtx: line_type = LineType.STMT else: line_type = LineType.NONE - if line_type != self._last_line_type and self._last_line_type != LineType.BLOCK_START and line_type != LineType.BLOCK_END: + if ( + line_type != self._last_line_type + and self._last_line_type != LineType.BLOCK_START + and line_type != LineType.BLOCK_END + ): self._result += "\n" self._last_line_type = line_type @@ -127,10 +129,10 @@ class DecompileCtx: for member in type.members.values(): if member.nick == value or member.c_ident == value: return member.name - return value.replace('-', '_') + return value.replace("-", "_") if type is None: - self.print(f"{name}: \"{escape_quote(value)}\";") + self.print(f'{name}: "{escape_quote(value)}";') elif type.assignable_to(FloatType()): self.print(f"{name}: {value};") elif type.assignable_to(BoolType()): @@ -139,12 +141,20 @@ class DecompileCtx: elif ( type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gdk.Pixbuf")) or type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gdk.Texture")) - or type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gdk.Paintable")) - or type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gtk.ShortcutAction")) - or type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gtk.ShortcutTrigger")) + or type.assignable_to( + self.gir.namespaces["Gtk"].lookup_type("Gdk.Paintable") + ) + or type.assignable_to( + self.gir.namespaces["Gtk"].lookup_type("Gtk.ShortcutAction") + ) + or type.assignable_to( + self.gir.namespaces["Gtk"].lookup_type("Gtk.ShortcutTrigger") + ) + ): + self.print(f'{name}: "{escape_quote(value)}";') + elif type.assignable_to( + self.gir.namespaces["Gtk"].lookup_type("GObject.Object") ): - self.print(f"{name}: \"{escape_quote(value)}\";") - elif type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("GObject.Object")): self.print(f"{name}: {value};") elif isinstance(type, Bitfield): flags = [get_enum_name(flag) for flag in value.split("|")] @@ -152,7 +162,7 @@ class DecompileCtx: elif isinstance(type, Enumeration): self.print(f"{name}: {get_enum_name(value)};") else: - self.print(f"{name}: \"{escape_quote(value)}\";") + self.print(f'{name}: "{escape_quote(value)}";') def _decompile_element(ctx: DecompileCtx, gir, xml): @@ -191,19 +201,21 @@ def decompile(data): return ctx.result - def canon(string: str) -> str: if string == "class": return "klass" else: return string.replace("-", "_").lower() + def truthy(string: str) -> bool: return string.lower() in ["yes", "true", "t", "y", "1"] + def full_name(gir): return gir.name if gir.full_name.startswith("Gtk.") else gir.full_name + def lookup_by_cname(gir, cname: str): if isinstance(gir, GirContext): return gir.get_type_by_cname(cname) @@ -216,15 +228,17 @@ def decompiler(tag, cdata=False): func._cdata = cdata _DECOMPILERS[tag] = func return func + return decorator def escape_quote(string: str) -> str: - return (string - .replace("\\", "\\\\") - .replace("\'", "\\'") - .replace("\"", "\\\"") - .replace("\n", "\\n")) + return ( + string.replace("\\", "\\\\") + .replace("'", "\\'") + .replace('"', '\\"') + .replace("\n", "\\n") + ) @decompiler("interface") @@ -243,7 +257,18 @@ def decompile_placeholder(ctx, gir): @decompiler("property", cdata=True) -def decompile_property(ctx, gir, name, cdata, bind_source=None, bind_property=None, bind_flags=None, translatable="false", comments=None, context=None): +def decompile_property( + ctx, + gir, + name, + cdata, + bind_source=None, + bind_property=None, + bind_flags=None, + translatable="false", + comments=None, + context=None, +): name = name.replace("_", "-") if comments is not None: ctx.print(f"/* Translators: {comments} */") @@ -263,18 +288,32 @@ def decompile_property(ctx, gir, name, cdata, bind_source=None, bind_property=No ctx.print(f"{name}: bind {bind_source}.{bind_property}{flags};") elif truthy(translatable): if context is not None: - ctx.print(f"{name}: C_(\"{escape_quote(context)}\", \"{escape_quote(cdata)}\");") + ctx.print( + f'{name}: C_("{escape_quote(context)}", "{escape_quote(cdata)}");' + ) else: - ctx.print(f"{name}: _(\"{escape_quote(cdata)}\");") + ctx.print(f'{name}: _("{escape_quote(cdata)}");') elif gir is None or gir.properties.get(name) is None: - ctx.print(f"{name}: \"{escape_quote(cdata)}\";") + ctx.print(f'{name}: "{escape_quote(cdata)}";') else: ctx.print_attribute(name, cdata, gir.properties.get(name).type) return gir + @decompiler("attribute", cdata=True) -def decompile_attribute(ctx, gir, name, cdata, translatable="false", comments=None, context=None): - decompile_property(ctx, gir, name, cdata, translatable=translatable, comments=comments, context=context) +def decompile_attribute( + ctx, gir, name, cdata, translatable="false", comments=None, context=None +): + decompile_property( + ctx, + gir, + name, + cdata, + translatable=translatable, + comments=comments, + context=context, + ) + @decompiler("attributes") def decompile_attributes(ctx, gir): @@ -291,5 +330,7 @@ class UnsupportedError(Exception): print(f"in {Colors.UNDERLINE}{filename}{Colors.NO_UNDERLINE}") if self.tag: print(f"in tag {Colors.BLUE}{self.tag}{Colors.CLEAR}") - print(f"""{Colors.FAINT}The compiler might support this feature, but the porting tool does not. You -probably need to port this file manually.{Colors.CLEAR}\n""") + print( + f"""{Colors.FAINT}The compiler might support this feature, but the porting tool does not. You +probably need to port this file manually.{Colors.CLEAR}\n""" + ) diff --git a/blueprintcompiler/errors.py b/blueprintcompiler/errors.py index e3bc787..7ddaef0 100644 --- a/blueprintcompiler/errors.py +++ b/blueprintcompiler/errors.py @@ -23,9 +23,10 @@ import sys, traceback from . import utils from .utils import Colors + class PrintableError(Exception): - """ Parent class for errors that can be pretty-printed for the user, e.g. - compilation warnings and errors. """ + """Parent class for errors that can be pretty-printed for the user, e.g. + compilation warnings and errors.""" def pretty_print(self, filename, code): raise NotImplementedError() @@ -39,12 +40,22 @@ class ErrorReference: class CompileError(PrintableError): - """ A PrintableError with a start/end position and optional hints """ + """A PrintableError with a start/end position and optional hints""" category = "error" color = Colors.RED - def __init__(self, message, start=None, end=None, did_you_mean=None, hints=None, actions=None, fatal=False, references=None): + def __init__( + self, + message, + start=None, + end=None, + did_you_mean=None, + hints=None, + actions=None, + fatal=False, + references=None, + ): super().__init__(message) self.message = message @@ -62,7 +73,6 @@ class CompileError(PrintableError): self.hints.append(hint) return self - def _did_you_mean(self, word: str, options: T.List[str]): if word.replace("_", "-") in options: self.hint(f"use '-', not '_': `{word.replace('_', '-')}`") @@ -86,9 +96,11 @@ class CompileError(PrintableError): # Display 1-based line numbers line_num += 1 - stream.write(f"""{self.color}{Colors.BOLD}{self.category}: {self.message}{Colors.CLEAR} + stream.write( + f"""{self.color}{Colors.BOLD}{self.category}: {self.message}{Colors.CLEAR} at {filename} line {line_num} column {col_num}: -{Colors.FAINT}{line_num :>4} |{Colors.CLEAR}{line.rstrip()}\n {Colors.FAINT}|{" "*(col_num-1)}^{Colors.CLEAR}\n""") +{Colors.FAINT}{line_num :>4} |{Colors.CLEAR}{line.rstrip()}\n {Colors.FAINT}|{" "*(col_num-1)}^{Colors.CLEAR}\n""" + ) for hint in self.hints: stream.write(f"{Colors.FAINT}hint: {hint}{Colors.CLEAR}\n") @@ -98,9 +110,11 @@ at {filename} line {line_num} column {col_num}: line = code.splitlines(True)[line_num] line_num += 1 - stream.write(f"""{Colors.FAINT}note: {ref.message}: + stream.write( + f"""{Colors.FAINT}note: {ref.message}: at {filename} line {line_num} column {col_num}: -{Colors.FAINT}{line_num :>4} |{line.rstrip()}\n {Colors.FAINT}|{" "*(col_num-1)}^{Colors.CLEAR}\n""") +{Colors.FAINT}{line_num :>4} |{line.rstrip()}\n {Colors.FAINT}|{" "*(col_num-1)}^{Colors.CLEAR}\n""" + ) stream.write("\n") @@ -122,9 +136,9 @@ class CodeAction: class MultipleErrors(PrintableError): - """ If multiple errors occur during compilation, they can be collected into + """If multiple errors occur during compilation, they can be collected into a list and re-thrown using the MultipleErrors exception. It will - pretty-print all of the errors and a count of how many errors there are. """ + pretty-print all of the errors and a count of how many errors there are.""" def __init__(self, errors: T.List[CompileError]): super().__init__() @@ -138,24 +152,25 @@ class MultipleErrors(PrintableError): class CompilerBugError(Exception): - """ Emitted on assertion errors """ + """Emitted on assertion errors""" -def assert_true(truth: bool, message: T.Optional[str]=None): +def assert_true(truth: bool, message: T.Optional[str] = None): if not truth: raise CompilerBugError(message) -def report_bug(): # pragma: no cover - """ Report an error and ask people to report it. """ +def report_bug(): # pragma: no cover + """Report an error and ask people to report it.""" print(traceback.format_exc()) print(f"Arguments: {sys.argv}\n") - print(f"""{Colors.BOLD}{Colors.RED}***** COMPILER BUG ***** + print( + f"""{Colors.BOLD}{Colors.RED}***** COMPILER BUG ***** The blueprint-compiler program has crashed. Please report the above stacktrace, along with the input file(s) if possible, on GitLab: {Colors.BOLD}{Colors.BLUE}{Colors.UNDERLINE}https://gitlab.gnome.org/jwestman/blueprint-compiler/-/issues/new?issue -{Colors.CLEAR}""") +{Colors.CLEAR}""" + ) sys.exit(1) - diff --git a/blueprintcompiler/gir.py b/blueprintcompiler/gir.py index 78335ee..a1bb419 100644 --- a/blueprintcompiler/gir.py +++ b/blueprintcompiler/gir.py @@ -21,9 +21,10 @@ from functools import cached_property import typing as T import os, sys -import gi # type: ignore +import gi # type: ignore + gi.require_version("GIRepository", "2.0") -from gi.repository import GIRepository # type: ignore +from gi.repository import GIRepository # type: ignore from .errors import CompileError, CompilerBugError from . import typelib, xml_reader @@ -60,10 +61,13 @@ def get_namespace(namespace, version) -> "Namespace": def get_xml(namespace, version): from .main import VERSION from xml.etree import ElementTree + search_paths = [] if data_paths := os.environ.get("XDG_DATA_DIRS"): - search_paths += [os.path.join(path, "gir-1.0") for path in data_paths.split(os.pathsep)] + search_paths += [ + os.path.join(path, "gir-1.0") for path in data_paths.split(os.pathsep) + ] filename = f"{namespace}-{version}.gir" @@ -104,36 +108,57 @@ class BasicType(GirType): def full_name(self) -> str: return self.name + class BoolType(BasicType): name = "bool" + def assignable_to(self, other) -> bool: return isinstance(other, BoolType) + class IntType(BasicType): name = "int" + def assignable_to(self, other) -> bool: - return isinstance(other, IntType) or isinstance(other, UIntType) or isinstance(other, FloatType) + return ( + isinstance(other, IntType) + or isinstance(other, UIntType) + or isinstance(other, FloatType) + ) + class UIntType(BasicType): name = "uint" + def assignable_to(self, other) -> bool: - return isinstance(other, IntType) or isinstance(other, UIntType) or isinstance(other, FloatType) + return ( + isinstance(other, IntType) + or isinstance(other, UIntType) + or isinstance(other, FloatType) + ) + class FloatType(BasicType): name = "float" + def assignable_to(self, other) -> bool: return isinstance(other, FloatType) + class StringType(BasicType): name = "string" + def assignable_to(self, other) -> bool: return isinstance(other, StringType) + class TypeType(BasicType): name = "GType" + def assignable_to(self, other) -> bool: return isinstance(other, TypeType) + _BASIC_TYPES = { "gboolean": BoolType, "int": IntType, @@ -150,6 +175,7 @@ _BASIC_TYPES = { "type": TypeType, } + class GirNode: def __init__(self, container, tl): self.container = container @@ -291,7 +317,9 @@ class Interface(GirNode, GirType): n_prerequisites = self.tl.INTERFACE_N_PREREQUISITES offset = self.tl.header.HEADER_INTERFACE_BLOB_SIZE offset += (n_prerequisites + n_prerequisites % 2) * 2 - offset += self.tl.INTERFACE_N_PROPERTIES * self.tl.header.HEADER_PROPERTY_BLOB_SIZE + offset += ( + self.tl.INTERFACE_N_PROPERTIES * self.tl.header.HEADER_PROPERTY_BLOB_SIZE + ) offset += self.tl.INTERFACE_N_METHODS * self.tl.header.HEADER_FUNCTION_BLOB_SIZE n_signals = self.tl.INTERFACE_N_SIGNALS property_size = self.tl.header.HEADER_SIGNAL_BLOB_SIZE @@ -342,7 +370,9 @@ class Class(GirNode, GirType): offset = self.tl.header.HEADER_OBJECT_BLOB_SIZE offset += (n_interfaces + n_interfaces % 2) * 2 offset += self.tl.OBJ_N_FIELDS * self.tl.header.HEADER_FIELD_BLOB_SIZE - offset += self.tl.OBJ_N_FIELD_CALLBACKS * self.tl.header.HEADER_CALLBACK_BLOB_SIZE + offset += ( + self.tl.OBJ_N_FIELD_CALLBACKS * self.tl.header.HEADER_CALLBACK_BLOB_SIZE + ) n_properties = self.tl.OBJ_N_PROPERTIES property_size = self.tl.header.HEADER_PROPERTY_BLOB_SIZE result = {} @@ -357,7 +387,9 @@ class Class(GirNode, GirType): offset = self.tl.header.HEADER_OBJECT_BLOB_SIZE offset += (n_interfaces + n_interfaces % 2) * 2 offset += self.tl.OBJ_N_FIELDS * self.tl.header.HEADER_FIELD_BLOB_SIZE - offset += self.tl.OBJ_N_FIELD_CALLBACKS * self.tl.header.HEADER_CALLBACK_BLOB_SIZE + offset += ( + self.tl.OBJ_N_FIELD_CALLBACKS * self.tl.header.HEADER_CALLBACK_BLOB_SIZE + ) offset += self.tl.OBJ_N_PROPERTIES * self.tl.header.HEADER_PROPERTY_BLOB_SIZE offset += self.tl.OBJ_N_METHODS * self.tl.header.HEADER_FUNCTION_BLOB_SIZE n_signals = self.tl.OBJ_N_SIGNALS @@ -381,16 +413,18 @@ class Class(GirNode, GirType): if self.parent is not None: result += f" : {self.parent.container.name}.{self.parent.name}" if len(self.implements): - result += " implements " + ", ".join([impl.full_name for impl in self.implements]) + result += " implements " + ", ".join( + [impl.full_name for impl in self.implements] + ) return result @cached_property def properties(self): - return { p.name: p for p in self._enum_properties() } + return {p.name: p for p in self._enum_properties()} @cached_property def signals(self): - return { s.name: s for s in self._enum_signals() } + return {s.name: s for s in self._enum_signals()} def assignable_to(self, other) -> bool: if self == other: @@ -509,9 +543,12 @@ class Namespace(GirNode): elif entry_type == typelib.BLOB_TYPE_OBJECT: self.entries[entry_name] = Class(self, entry_blob) elif entry_type == typelib.BLOB_TYPE_INTERFACE: - self.entries[entry_name] = Interface(self, entry_blob) - elif entry_type == typelib.BLOB_TYPE_BOXED or entry_type == typelib.BLOB_TYPE_STRUCT: - self.entries[entry_name] = Boxed(self, entry_blob) + self.entries[entry_name] = Interface(self, entry_blob) + elif ( + entry_type == typelib.BLOB_TYPE_BOXED + or entry_type == typelib.BLOB_TYPE_STRUCT + ): + self.entries[entry_name] = Boxed(self, entry_blob) @cached_property def xml(self): @@ -531,25 +568,33 @@ class Namespace(GirNode): @cached_property def classes(self): - return { name: entry for name, entry in self.entries.items() if isinstance(entry, Class) } + return { + name: entry + for name, entry in self.entries.items() + if isinstance(entry, Class) + } @cached_property def interfaces(self): - return { name: entry for name, entry in self.entries.items() if isinstance(entry, Interface) } + return { + name: entry + for name, entry in self.entries.items() + if isinstance(entry, Interface) + } def get_type(self, name): - """ Gets a type (class, interface, enum, etc.) from this namespace. """ + """Gets a type (class, interface, enum, etc.) from this namespace.""" return self.entries.get(name) def get_type_by_cname(self, cname: str): - """ Gets a type from this namespace by its C name. """ + """Gets a type from this namespace by its C name.""" for item in self.entries.values(): if hasattr(item, "cname") and item.cname == cname: return item def lookup_type(self, type_name: str): - """ Looks up a type in the scope of this namespace (including in the - namespace's dependencies). """ + """Looks up a type in the scope of this namespace (including in the + namespace's dependencies).""" if type_name in _BASIC_TYPES: return _BASIC_TYPES[type_name]() @@ -569,7 +614,9 @@ class Repository(GirNode): if dependencies := tl[0x24].string: deps = [tuple(dep.split("-", 1)) for dep in dependencies.split("|")] try: - self.includes = { name: get_namespace(name, version) for name, version in deps } + self.includes = { + name: get_namespace(name, version) for name, version in deps + } except: raise CompilerBugError(f"Failed to load dependencies.") else: @@ -578,16 +625,14 @@ class Repository(GirNode): def get_type(self, name: str, ns: str) -> T.Optional[GirNode]: return self.lookup_namespace(ns).get_type(name) - def get_type_by_cname(self, name: str) -> T.Optional[GirNode]: for ns in [self.namespace, *self.includes.values()]: if type := ns.get_type_by_cname(name): return type return None - def lookup_namespace(self, ns: str): - """ Finds a namespace among this namespace's dependencies. """ + """Finds a namespace among this namespace's dependencies.""" if ns == self.namespace.name: return self.namespace else: @@ -610,9 +655,19 @@ class Repository(GirNode): return BoolType() elif type_id in [typelib.TYPE_FLOAT, typelib.TYPE_DOUBLE]: return FloatType() - elif type_id in [typelib.TYPE_INT8, typelib.TYPE_INT16, typelib.TYPE_INT32, typelib.TYPE_INT64]: + elif type_id in [ + typelib.TYPE_INT8, + typelib.TYPE_INT16, + typelib.TYPE_INT32, + typelib.TYPE_INT64, + ]: return IntType() - elif type_id in [typelib.TYPE_UINT8, typelib.TYPE_UINT16, typelib.TYPE_UINT32, typelib.TYPE_UINT64]: + elif type_id in [ + typelib.TYPE_UINT8, + typelib.TYPE_UINT16, + typelib.TYPE_UINT32, + typelib.TYPE_UINT64, + ]: return UIntType() elif type_id == typelib.TYPE_UTF8: return StringType() @@ -621,30 +676,30 @@ class Repository(GirNode): else: raise CompilerBugError("Unknown type ID", type_id) else: - return self._resolve_dir_entry(self.tl.header[type_id].INTERFACE_TYPE_INTERFACE) - + return self._resolve_dir_entry( + self.tl.header[type_id].INTERFACE_TYPE_INTERFACE + ) class GirContext: def __init__(self): self.namespaces = {} - def add_namespace(self, namespace: Namespace): other = self.namespaces.get(namespace.name) if other is not None and other.version != namespace.version: - raise CompileError(f"Namespace {namespace.name}-{namespace.version} can't be imported because version {other.version} was imported earlier") + raise CompileError( + f"Namespace {namespace.name}-{namespace.version} can't be imported because version {other.version} was imported earlier" + ) self.namespaces[namespace.name] = namespace - def get_type_by_cname(self, name: str) -> T.Optional[GirNode]: for ns in self.namespaces.values(): if type := ns.get_type_by_cname(name): return type return None - def get_type(self, name: str, ns: str) -> T.Optional[GirNode]: ns = ns or "Gtk" @@ -653,7 +708,6 @@ class GirContext: return self.namespaces[ns].get_type(name) - def get_class(self, name: str, ns: str) -> T.Optional[Class]: type = self.get_type(name, ns) if isinstance(type, Class): @@ -661,10 +715,9 @@ class GirContext: else: return None - def validate_ns(self, ns: str): - """ Raises an exception if there is a problem looking up the given - namespace. """ + """Raises an exception if there is a problem looking up the given + namespace.""" ns = ns or "Gtk" @@ -675,7 +728,7 @@ class GirContext: ) def validate_type(self, name: str, ns: str): - """ Raises an exception if there is a problem looking up the given type. """ + """Raises an exception if there is a problem looking up the given type.""" self.validate_ns(ns) diff --git a/blueprintcompiler/interactive_port.py b/blueprintcompiler/interactive_port.py index 229f238..dd00317 100644 --- a/blueprintcompiler/interactive_port.py +++ b/blueprintcompiler/interactive_port.py @@ -35,9 +35,11 @@ class CouldNotPort: def __init__(self, message): self.message = message + def change_suffix(f): return f.removesuffix(".ui") + ".blp" + def decompile_file(in_file, out_file) -> T.Union[str, CouldNotPort]: if os.path.exists(out_file): return CouldNotPort("already exists") @@ -63,12 +65,15 @@ def decompile_file(in_file, out_file) -> T.Union[str, CouldNotPort]: except PrintableError as e: e.pretty_print(out_file, decompiled) - print(f"{Colors.RED}{Colors.BOLD}error: the generated file does not compile{Colors.CLEAR}") + print( + f"{Colors.RED}{Colors.BOLD}error: the generated file does not compile{Colors.CLEAR}" + ) print(f"in {Colors.UNDERLINE}{out_file}{Colors.NO_UNDERLINE}") print( -f"""{Colors.FAINT}Either the original XML file had an error, or there is a bug in the + f"""{Colors.FAINT}Either the original XML file had an error, or there is a bug in the porting tool. If you think it's a bug (which is likely), please file an issue on GitLab: -{Colors.BLUE}{Colors.UNDERLINE}https://gitlab.gnome.org/jwestman/blueprint-compiler/-/issues/new?issue{Colors.CLEAR}\n""") +{Colors.BLUE}{Colors.UNDERLINE}https://gitlab.gnome.org/jwestman/blueprint-compiler/-/issues/new?issue{Colors.CLEAR}\n""" + ) return CouldNotPort("does not compile") @@ -108,7 +113,9 @@ def enter(): def step1(): - print(f"{Colors.BOLD}STEP 1: Create subprojects/blueprint-compiler.wrap{Colors.CLEAR}") + print( + f"{Colors.BOLD}STEP 1: Create subprojects/blueprint-compiler.wrap{Colors.CLEAR}" + ) if os.path.exists("subprojects/blueprint-compiler.wrap"): print("subprojects/blueprint-compiler.wrap already exists, skipping\n") @@ -121,17 +128,20 @@ def step1(): pass from .main import VERSION + VERSION = "main" if VERSION == "uninstalled" else "v" + VERSION with open("subprojects/blueprint-compiler.wrap", "w") as wrap: - wrap.write(f"""[wrap-git] + wrap.write( + f"""[wrap-git] directory = blueprint-compiler url = https://gitlab.gnome.org/jwestman/blueprint-compiler.git revision = {VERSION} depth = 1 [provide] -program_names = blueprint-compiler""") +program_names = blueprint-compiler""" + ) print() @@ -146,7 +156,9 @@ def step2(): if yesno("Add '/subprojects/blueprint-compiler' to .gitignore?"): gitignore.write("\n/subprojects/blueprint-compiler\n") else: - print("'/subprojects/blueprint-compiler' already in .gitignore, skipping") + print( + "'/subprojects/blueprint-compiler' already in .gitignore, skipping" + ) else: if yesno("Create .gitignore with '/subprojects/blueprint-compiler'?"): with open(".gitignore", "w") as gitignore: @@ -169,9 +181,13 @@ def step3(): if isinstance(result, CouldNotPort): if result.message == "already exists": print(Colors.FAINT, end="") - print(f"{Colors.RED}will not port {Colors.UNDERLINE}{in_file}{Colors.NO_UNDERLINE} -> {Colors.UNDERLINE}{out_file}{Colors.NO_UNDERLINE} [{result.message}]{Colors.CLEAR}") + print( + f"{Colors.RED}will not port {Colors.UNDERLINE}{in_file}{Colors.NO_UNDERLINE} -> {Colors.UNDERLINE}{out_file}{Colors.NO_UNDERLINE} [{result.message}]{Colors.CLEAR}" + ) else: - print(f"will port {Colors.UNDERLINE}{in_file}{Colors.CLEAR} -> {Colors.UNDERLINE}{out_file}{Colors.CLEAR}") + print( + f"will port {Colors.UNDERLINE}{in_file}{Colors.CLEAR} -> {Colors.UNDERLINE}{out_file}{Colors.CLEAR}" + ) success += 1 print() @@ -180,7 +196,9 @@ def step3(): elif success == len(files): print(f"{Colors.GREEN}All files were converted.{Colors.CLEAR}") elif success > 0: - print(f"{Colors.RED}{success} file(s) were converted, {len(files) - success} were not.{Colors.CLEAR}") + print( + f"{Colors.RED}{success} file(s) were converted, {len(files) - success} were not.{Colors.CLEAR}" + ) else: print(f"{Colors.RED}None of the files could be converted.{Colors.CLEAR}") @@ -204,22 +222,33 @@ def step3(): def step4(ported): print(f"{Colors.BOLD}STEP 4: Set up meson.build{Colors.CLEAR}") - print(f"{Colors.BOLD}{Colors.YELLOW}NOTE: Depending on your build system setup, you may need to make some adjustments to this step.{Colors.CLEAR}") + print( + f"{Colors.BOLD}{Colors.YELLOW}NOTE: Depending on your build system setup, you may need to make some adjustments to this step.{Colors.CLEAR}" + ) - meson_files = [file for file in listdir_recursive(".") if os.path.basename(file) == "meson.build"] + meson_files = [ + file + for file in listdir_recursive(".") + if os.path.basename(file) == "meson.build" + ] for meson_file in meson_files: with open(meson_file, "r") as f: if "gnome.compile_resources" in f.read(): parent = os.path.dirname(meson_file) - file_list = "\n ".join([ - f"'{os.path.relpath(file, parent)}'," - for file in ported - if file.startswith(parent) - ]) + file_list = "\n ".join( + [ + f"'{os.path.relpath(file, parent)}'," + for file in ported + if file.startswith(parent) + ] + ) if len(file_list): - print(f"{Colors.BOLD}Paste the following into {Colors.UNDERLINE}{meson_file}{Colors.NO_UNDERLINE}:{Colors.CLEAR}") - print(f""" + print( + f"{Colors.BOLD}Paste the following into {Colors.UNDERLINE}{meson_file}{Colors.NO_UNDERLINE}:{Colors.CLEAR}" + ) + print( + f""" blueprints = custom_target('blueprints', input: files( {file_list} @@ -227,14 +256,17 @@ blueprints = custom_target('blueprints', output: '.', command: [find_program('blueprint-compiler'), 'batch-compile', '@OUTPUT@', '@CURRENT_SOURCE_DIR@', '@INPUT@'], ) -""") +""" + ) enter() - print(f"""{Colors.BOLD}Paste the following into the 'gnome.compile_resources()' + print( + f"""{Colors.BOLD}Paste the following into the 'gnome.compile_resources()' arguments in {Colors.UNDERLINE}{meson_file}{Colors.NO_UNDERLINE}:{Colors.CLEAR} dependencies: blueprints, - """) + """ + ) enter() print() @@ -244,7 +276,9 @@ def step5(in_files): print(f"{Colors.BOLD}STEP 5: Update POTFILES.in{Colors.CLEAR}") if not os.path.exists("po/POTFILES.in"): - print(f"{Colors.UNDERLINE}po/POTFILES.in{Colors.NO_UNDERLINE} does not exist, skipping\n") + print( + f"{Colors.UNDERLINE}po/POTFILES.in{Colors.NO_UNDERLINE} does not exist, skipping\n" + ) return with open("po/POTFILES.in", "r") as potfiles: @@ -257,12 +291,24 @@ def step5(in_files): new_data = "".join(lines) - print(f"{Colors.BOLD}Will make the following changes to {Colors.UNDERLINE}po/POTFILES.in{Colors.CLEAR}") print( - "".join([ - (Colors.GREEN if line.startswith('+') else Colors.RED + Colors.FAINT if line.startswith('-') else '') + line + Colors.CLEAR - for line in difflib.unified_diff(old_lines, lines) - ]) + f"{Colors.BOLD}Will make the following changes to {Colors.UNDERLINE}po/POTFILES.in{Colors.CLEAR}" + ) + print( + "".join( + [ + ( + Colors.GREEN + if line.startswith("+") + else Colors.RED + Colors.FAINT + if line.startswith("-") + else "" + ) + + line + + Colors.CLEAR + for line in difflib.unified_diff(old_lines, lines) + ] + ) ) if yesno("Is this ok?"): @@ -291,5 +337,6 @@ def run(opts): step5(in_files) step6(in_files) - print(f"{Colors.BOLD}STEP 6: Done! Make sure your app still builds and runs correctly.{Colors.CLEAR}") - + print( + f"{Colors.BOLD}STEP 6: Done! Make sure your app still builds and runs correctly.{Colors.CLEAR}" + ) diff --git a/blueprintcompiler/language/__init__.py b/blueprintcompiler/language/__init__.py index da5a44c..58662b2 100644 --- a/blueprintcompiler/language/__init__.py +++ b/blueprintcompiler/language/__init__.py @@ -16,7 +16,16 @@ from .gtkbuilder_template import Template from .imports import GtkDirective, Import from .ui import UI from .types import ClassName -from .values import TypeValue, IdentValue, TranslatedStringValue, FlagsValue, Flag, QuotedValue, NumberValue, Value +from .values import ( + TypeValue, + IdentValue, + TranslatedStringValue, + FlagsValue, + Flag, + QuotedValue, + NumberValue, + Value, +) from .common import * diff --git a/blueprintcompiler/language/attributes.py b/blueprintcompiler/language/attributes.py index 66faa60..77f01f2 100644 --- a/blueprintcompiler/language/attributes.py +++ b/blueprintcompiler/language/attributes.py @@ -23,7 +23,7 @@ from .common import * class BaseAttribute(AstNode): - """ A helper class for attribute syntax of the form `name: literal_value;`""" + """A helper class for attribute syntax of the form `name: literal_value;`""" tag_name: str = "" attr_name: str = "name" @@ -34,5 +34,5 @@ class BaseAttribute(AstNode): class BaseTypedAttribute(BaseAttribute): - """ A BaseAttribute whose parent has a value_type property that can assist - in validation. """ + """A BaseAttribute whose parent has a value_type property that can assist + in validation.""" diff --git a/blueprintcompiler/language/gobject_object.py b/blueprintcompiler/language/gobject_object.py index 2647a5a..8e6fd23 100644 --- a/blueprintcompiler/language/gobject_object.py +++ b/blueprintcompiler/language/gobject_object.py @@ -33,6 +33,7 @@ class ObjectContent(AstNode): def gir_class(self): return self.parent.gir_class + class Object(AstNode): grammar: T.Any = [ ConcreteClassName, @@ -75,13 +76,17 @@ def validate_parent_type(node, ns: str, name: str, err_msg: str): parent = node.root.gir.get_type(name, ns) container_type = node.parent_by_type(Object).gir_class if container_type and not container_type.assignable_to(parent): - raise CompileError(f"{container_type.full_name} is not a {parent.full_name}, so it doesn't have {err_msg}") + raise CompileError( + f"{container_type.full_name} is not a {parent.full_name}, so it doesn't have {err_msg}" + ) @decompiler("object") def decompile_object(ctx, gir, klass, id=None): gir_class = ctx.type_by_cname(klass) - klass_name = decompile.full_name(gir_class) if gir_class is not None else "." + klass + klass_name = ( + decompile.full_name(gir_class) if gir_class is not None else "." + klass + ) if id is None: ctx.print(f"{klass_name} {{") else: diff --git a/blueprintcompiler/language/gobject_property.py b/blueprintcompiler/language/gobject_property.py index 4d47e80..2c1e2ae 100644 --- a/blueprintcompiler/language/gobject_property.py +++ b/blueprintcompiler/language/gobject_property.py @@ -34,12 +34,16 @@ class Property(AstNode): 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"), - )), + 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( @@ -63,19 +67,16 @@ class Property(AstNode): def gir_class(self): return self.parent.parent.gir_class - @property def gir_property(self): if self.gir_class is not None: return self.gir_class.properties.get(self.tokens["name"]) - @property def value_type(self): if self.gir_property is not None: return self.gir_property.type - @validate("name") def property_exists(self): if self.gir_class is None: @@ -91,15 +92,19 @@ class Property(AstNode): if self.gir_property is None: raise CompileError( f"Class {self.gir_class.full_name} does not contain a property called {self.tokens['name']}", - did_you_mean=(self.tokens["name"], self.gir_class.properties.keys()) + 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: + 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"] + hints=["construct-only properties may only be set to a static value"], ) @validate("name") @@ -107,7 +112,6 @@ class Property(AstNode): 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: @@ -115,20 +119,23 @@ class Property(AstNode): 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): + 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( f"Duplicate property '{self.tokens['name']}'", - check=lambda child: child.tokens["name"] == self.tokens["name"] + check=lambda child: child.tokens["name"] == self.tokens["name"], ) - @docs("name") def property_docs(self): if self.gir_property is not None: diff --git a/blueprintcompiler/language/gobject_signal.py b/blueprintcompiler/language/gobject_signal.py index 1cba801..d50792e 100644 --- a/blueprintcompiler/language/gobject_signal.py +++ b/blueprintcompiler/language/gobject_signal.py @@ -26,19 +26,23 @@ from .common import * class Signal(AstNode): grammar = Statement( UseIdent("name"), - Optional([ - "::", - UseIdent("detail_name").expected("a signal detail name"), - ]), + Optional( + [ + "::", + UseIdent("detail_name").expected("a signal detail name"), + ] + ), "=>", UseIdent("handler").expected("the name of a function to handle the signal"), Match("(").expected("argument list"), Optional(UseIdent("object")).expected("object identifier"), Match(")").expected(), - ZeroOrMore(AnyOf( - [Keyword("swapped"), UseLiteral("swapped", True)], - [Keyword("after"), UseLiteral("after", True)], - )), + ZeroOrMore( + AnyOf( + [Keyword("swapped"), UseLiteral("swapped", True)], + [Keyword("after"), UseLiteral("after", True)], + ) + ), ) @property @@ -65,18 +69,15 @@ class Signal(AstNode): def is_after(self) -> bool: return self.tokens["after"] or False - @property def gir_signal(self): if self.gir_class is not None: return self.gir_class.signals.get(self.tokens["name"]) - @property def gir_class(self): return self.parent.parent.gir_class - @validate("name") def signal_exists(self): if self.gir_class is None: @@ -92,10 +93,9 @@ class Signal(AstNode): if self.gir_signal is None: raise CompileError( f"Class {self.gir_class.full_name} does not contain a signal called {self.tokens['name']}", - did_you_mean=(self.tokens["name"], self.gir_class.signals.keys()) + did_you_mean=(self.tokens["name"], self.gir_class.signals.keys()), ) - @validate("object") def object_exists(self): object_id = self.tokens["object"] @@ -103,10 +103,7 @@ class Signal(AstNode): return if self.root.objects_by_id.get(object_id) is None: - raise CompileError( - f"Could not find object with ID '{object_id}'" - ) - + raise CompileError(f"Could not find object with ID '{object_id}'") @docs("name") def signal_docs(self): diff --git a/blueprintcompiler/language/gtk_a11y.py b/blueprintcompiler/language/gtk_a11y.py index cab72e0..f2066ff 100644 --- a/blueprintcompiler/language/gtk_a11y.py +++ b/blueprintcompiler/language/gtk_a11y.py @@ -86,6 +86,7 @@ def get_state_types(gir): "selected": BoolType(), } + def get_types(gir): return { **get_property_types(gir), @@ -93,6 +94,7 @@ def get_types(gir): **get_state_types(gir), } + def _get_docs(gir, name): if gir_type := ( gir.get_type("AccessibleProperty", "Gtk").members.get(name) @@ -174,8 +176,7 @@ class A11y(AstNode): ) def a11y_completer(ast_node, match_variables): yield Completion( - "accessibility", CompletionItemKind.Snippet, - snippet="accessibility {\n $0\n}" + "accessibility", CompletionItemKind.Snippet, snippet="accessibility {\n $0\n}" ) @@ -185,20 +186,24 @@ def a11y_completer(ast_node, match_variables): ) def a11y_name_completer(ast_node, match_variables): for name, type in get_types(ast_node.root.gir).items(): - yield Completion(name, CompletionItemKind.Property, docs=_get_docs(ast_node.root.gir, type)) + yield Completion( + name, CompletionItemKind.Property, docs=_get_docs(ast_node.root.gir, type) + ) @decompiler("relation", cdata=True) def decompile_relation(ctx, gir, name, cdata): ctx.print_attribute(name, cdata, get_types(ctx.gir).get(name)) + @decompiler("state", cdata=True) def decompile_state(ctx, gir, name, cdata, translatable="false"): if decompile.truthy(translatable): - ctx.print(f"{name}: _(\"{_escape_quote(cdata)}\");") + ctx.print(f'{name}: _("{_escape_quote(cdata)}");') else: ctx.print_attribute(name, cdata, get_types(ctx.gir).get(name)) + @decompiler("accessibility") def decompile_accessibility(ctx, gir): ctx.print("accessibility {") diff --git a/blueprintcompiler/language/gtk_combo_box_text.py b/blueprintcompiler/language/gtk_combo_box_text.py index 31d2d08..f0f6f37 100644 --- a/blueprintcompiler/language/gtk_combo_box_text.py +++ b/blueprintcompiler/language/gtk_combo_box_text.py @@ -35,12 +35,14 @@ class Item(BaseTypedAttribute): item = Group( Item, [ - Optional([ - UseIdent("name"), - ":", - ]), + Optional( + [ + UseIdent("name"), + ":", + ] + ), VALUE_HOOKS, - ] + ], ) @@ -67,7 +69,4 @@ class Items(AstNode): matches=new_statement_patterns, ) def items_completer(ast_node, match_variables): - yield Completion( - "items", CompletionItemKind.Snippet, - snippet="items [$0]" - ) + yield Completion("items", CompletionItemKind.Snippet, snippet="items [$0]") diff --git a/blueprintcompiler/language/gtk_file_filter.py b/blueprintcompiler/language/gtk_file_filter.py index 4419311..9625094 100644 --- a/blueprintcompiler/language/gtk_file_filter.py +++ b/blueprintcompiler/language/gtk_file_filter.py @@ -37,6 +37,7 @@ class Filters(AstNode): f"Duplicate {self.tokens['tag_name']} block", check=lambda child: child.tokens["tag_name"] == self.tokens["tag_name"], ) + wrapped_validator(self) @@ -57,12 +58,12 @@ def create_node(tag_name: str, singular: str): [ UseQuoted("name"), UseLiteral("tag_name", singular), - ] + ], ), ",", ), "]", - ] + ], ) @@ -77,31 +78,38 @@ suffixes = create_node("suffixes", "suffix") matches=new_statement_patterns, ) def file_filter_completer(ast_node, match_variables): - yield Completion("mime-types", CompletionItemKind.Snippet, snippet="mime-types [\"$0\"]") - yield Completion("patterns", CompletionItemKind.Snippet, snippet="patterns [\"$0\"]") - yield Completion("suffixes", CompletionItemKind.Snippet, snippet="suffixes [\"$0\"]") + yield Completion( + "mime-types", CompletionItemKind.Snippet, snippet='mime-types ["$0"]' + ) + yield Completion("patterns", CompletionItemKind.Snippet, snippet='patterns ["$0"]') + yield Completion("suffixes", CompletionItemKind.Snippet, snippet='suffixes ["$0"]') @decompiler("mime-types") def decompile_mime_types(ctx, gir): ctx.print("mime-types [") + @decompiler("mime-type", cdata=True) def decompile_mime_type(ctx, gir, cdata): ctx.print(f'"{cdata}",') + @decompiler("patterns") def decompile_patterns(ctx, gir): ctx.print("patterns [") + @decompiler("pattern", cdata=True) def decompile_pattern(ctx, gir, cdata): ctx.print(f'"{cdata}",') + @decompiler("suffixes") def decompile_suffixes(ctx, gir): ctx.print("suffixes [") + @decompiler("suffix", cdata=True) def decompile_suffix(ctx, gir, cdata): ctx.print(f'"{cdata}",') diff --git a/blueprintcompiler/language/gtk_layout.py b/blueprintcompiler/language/gtk_layout.py index b52f7bc..9af82fd 100644 --- a/blueprintcompiler/language/gtk_layout.py +++ b/blueprintcompiler/language/gtk_layout.py @@ -45,7 +45,7 @@ layout_prop = Group( UseIdent("name"), ":", VALUE_HOOKS.expected("a value"), - ) + ), ) @@ -71,10 +71,7 @@ class Layout(AstNode): matches=new_statement_patterns, ) def layout_completer(ast_node, match_variables): - yield Completion( - "layout", CompletionItemKind.Snippet, - snippet="layout {\n $0\n}" - ) + yield Completion("layout", CompletionItemKind.Snippet, snippet="layout {\n $0\n}") @decompiler("layout") diff --git a/blueprintcompiler/language/gtk_menu.py b/blueprintcompiler/language/gtk_menu.py index 355997a..3d3e6ee 100644 --- a/blueprintcompiler/language/gtk_menu.py +++ b/blueprintcompiler/language/gtk_menu.py @@ -56,22 +56,12 @@ menu_contents = Sequence() menu_section = Group( Menu, - [ - "section", - UseLiteral("tag", "section"), - Optional(UseIdent("id")), - menu_contents - ] + ["section", UseLiteral("tag", "section"), Optional(UseIdent("id")), menu_contents], ) menu_submenu = Group( Menu, - [ - "submenu", - UseLiteral("tag", "submenu"), - Optional(UseIdent("id")), - menu_contents - ] + ["submenu", UseLiteral("tag", "submenu"), Optional(UseIdent("id")), menu_contents], ) menu_attribute = Group( @@ -81,7 +71,7 @@ menu_attribute = Group( ":", VALUE_HOOKS.expected("a value"), Match(";").expected(), - ] + ], ) menu_item = Group( @@ -92,7 +82,7 @@ menu_item = Group( Optional(UseIdent("id")), Match("{").expected(), Until(menu_attribute, "}"), - ] + ], ) menu_item_shorthand = Group( @@ -105,58 +95,60 @@ menu_item_shorthand = Group( MenuAttribute, [UseLiteral("name", "label"), VALUE_HOOKS], ), - Optional([ - ",", - Optional([ - Group( - MenuAttribute, - [UseLiteral("name", "action"), VALUE_HOOKS], + Optional( + [ + ",", + Optional( + [ + Group( + MenuAttribute, + [UseLiteral("name", "action"), VALUE_HOOKS], + ), + Optional( + [ + ",", + Group( + MenuAttribute, + [UseLiteral("name", "icon"), VALUE_HOOKS], + ), + ] + ), + ] ), - Optional([ - ",", - Group( - MenuAttribute, - [UseLiteral("name", "icon"), VALUE_HOOKS], - ), - ]) - ]) - ]), + ] + ), Match(")").expected(), - ] + ], ) menu_contents.children = [ Match("{"), - Until(AnyOf( - menu_section, - menu_submenu, - menu_item_shorthand, - menu_item, - menu_attribute, - ), "}"), + Until( + AnyOf( + menu_section, + menu_submenu, + menu_item_shorthand, + menu_item, + menu_attribute, + ), + "}", + ), ] menu: Group = Group( Menu, - [ - "menu", - UseLiteral("tag", "menu"), - Optional(UseIdent("id")), - menu_contents - ], + ["menu", UseLiteral("tag", "menu"), Optional(UseIdent("id")), menu_contents], ) from .ui import UI + @completer( applies_in=[UI], matches=new_statement_patterns, ) def menu_completer(ast_node, match_variables): - yield Completion( - "menu", CompletionItemKind.Snippet, - snippet="menu {\n $0\n}" - ) + yield Completion("menu", CompletionItemKind.Snippet, snippet="menu {\n $0\n}") @completer( @@ -165,34 +157,21 @@ def menu_completer(ast_node, match_variables): ) def menu_content_completer(ast_node, match_variables): yield Completion( - "submenu", CompletionItemKind.Snippet, - snippet="submenu {\n $0\n}" + "submenu", CompletionItemKind.Snippet, snippet="submenu {\n $0\n}" ) yield Completion( - "section", CompletionItemKind.Snippet, - snippet="section {\n $0\n}" + "section", CompletionItemKind.Snippet, snippet="section {\n $0\n}" ) + yield Completion("item", CompletionItemKind.Snippet, snippet="item {\n $0\n}") yield Completion( - "item", CompletionItemKind.Snippet, - snippet="item {\n $0\n}" - ) - yield Completion( - "item (shorthand)", CompletionItemKind.Snippet, - snippet='item (_("${1:Label}"), "${2:action-name}", "${3:icon-name}")' + "item (shorthand)", + CompletionItemKind.Snippet, + snippet='item (_("${1:Label}"), "${2:action-name}", "${3:icon-name}")', ) - yield Completion( - "label", CompletionItemKind.Snippet, - snippet='label: $0;' - ) - yield Completion( - "action", CompletionItemKind.Snippet, - snippet='action: "$0";' - ) - yield Completion( - "icon", CompletionItemKind.Snippet, - snippet='icon: "$0";' - ) + yield Completion("label", CompletionItemKind.Snippet, snippet="label: $0;") + yield Completion("action", CompletionItemKind.Snippet, snippet='action: "$0";') + yield Completion("icon", CompletionItemKind.Snippet, snippet='icon: "$0";') @decompiler("menu") @@ -202,6 +181,7 @@ def decompile_menu(ctx, gir, id=None): else: ctx.print("menu {") + @decompiler("submenu") def decompile_submenu(ctx, gir, id=None): if id: @@ -209,6 +189,7 @@ def decompile_submenu(ctx, gir, id=None): else: ctx.print("submenu {") + @decompiler("item") def decompile_item(ctx, gir, id=None): if id: @@ -216,6 +197,7 @@ def decompile_item(ctx, gir, id=None): else: ctx.print("item {") + @decompiler("section") def decompile_section(ctx, gir, id=None): if id: diff --git a/blueprintcompiler/language/gtk_size_group.py b/blueprintcompiler/language/gtk_size_group.py index eb56043..80c47af 100644 --- a/blueprintcompiler/language/gtk_size_group.py +++ b/blueprintcompiler/language/gtk_size_group.py @@ -32,7 +32,7 @@ class Widget(AstNode): if object is None: raise CompileError( f"Could not find object with ID {self.tokens['name']}", - did_you_mean=(self.tokens['name'], self.root.objects_by_id.keys()), + did_you_mean=(self.tokens["name"], self.root.objects_by_id.keys()), ) elif object.gir_class and not object.gir_class.assignable_to(type): raise CompileError( diff --git a/blueprintcompiler/language/gtk_string_list.py b/blueprintcompiler/language/gtk_string_list.py index e7eb0f8..347b9e8 100644 --- a/blueprintcompiler/language/gtk_string_list.py +++ b/blueprintcompiler/language/gtk_string_list.py @@ -55,7 +55,4 @@ class Strings(AstNode): matches=new_statement_patterns, ) def strings_completer(ast_node, match_variables): - yield Completion( - "strings", CompletionItemKind.Snippet, - snippet="strings [$0]" - ) + yield Completion("strings", CompletionItemKind.Snippet, snippet="strings [$0]") diff --git a/blueprintcompiler/language/gtk_styles.py b/blueprintcompiler/language/gtk_styles.py index 644d8a3..1005568 100644 --- a/blueprintcompiler/language/gtk_styles.py +++ b/blueprintcompiler/language/gtk_styles.py @@ -49,13 +49,14 @@ class Styles(AstNode): matches=new_statement_patterns, ) def style_completer(ast_node, match_variables): - yield Completion("styles", CompletionItemKind.Keyword, snippet="styles [\"$0\"]") + yield Completion("styles", CompletionItemKind.Keyword, snippet='styles ["$0"]') @decompiler("style") def decompile_style(ctx, gir): ctx.print(f"styles [") + @decompiler("class") def decompile_style_class(ctx, gir, name): ctx.print(f'"{name}",') diff --git a/blueprintcompiler/language/gtkbuilder_child.py b/blueprintcompiler/language/gtkbuilder_child.py index 71efe19..45b2074 100644 --- a/blueprintcompiler/language/gtkbuilder_child.py +++ b/blueprintcompiler/language/gtkbuilder_child.py @@ -26,18 +26,21 @@ from .common import * ALLOWED_PARENTS: T.List[T.Tuple[str, str]] = [ ("Gtk", "Buildable"), - ("Gio", "ListStore") + ("Gio", "ListStore"), ] + class Child(AstNode): grammar = [ - Optional([ - "[", - Optional(["internal-child", UseLiteral("internal_child", True)]), - UseIdent("child_type").expected("a child type"), - Optional(ResponseId), - "]", - ]), + Optional( + [ + "[", + Optional(["internal-child", UseLiteral("internal_child", True)]), + UseIdent("child_type").expected("a child type"), + Optional(ResponseId), + "]", + ] + ), Object, ] @@ -53,9 +56,13 @@ class Child(AstNode): if gir_class.assignable_to(parent_type): break else: - hints=["only Gio.ListStore or Gtk.Buildable implementors can have children"] + hints = [ + "only Gio.ListStore or Gtk.Buildable implementors can have children" + ] if "child" in gir_class.properties: - hints.append("did you mean to assign this object to the 'child' property?") + hints.append( + "did you mean to assign this object to the 'child' property?" + ) raise CompileError( f"{gir_class.full_name} doesn't have children", hints=hints, diff --git a/blueprintcompiler/language/gtkbuilder_template.py b/blueprintcompiler/language/gtkbuilder_template.py index 7392782..a215cdf 100644 --- a/blueprintcompiler/language/gtkbuilder_template.py +++ b/blueprintcompiler/language/gtkbuilder_template.py @@ -28,10 +28,12 @@ class Template(Object): grammar = [ "template", UseIdent("id").expected("template class name"), - Optional([ - Match(":"), - to_parse_node(ClassName).expected("parent class"), - ]), + Optional( + [ + Match(":"), + to_parse_node(ClassName).expected("parent class"), + ] + ), ObjectContent, ] @@ -54,7 +56,9 @@ class Template(Object): @validate("id") def unique_in_parent(self): - self.validate_unique_in_parent(f"Only one template may be defined per file, but this file contains {len(self.parent.children[Template])}",) + self.validate_unique_in_parent( + f"Only one template may be defined per file, but this file contains {len(self.parent.children[Template])}", + ) @decompiler("template") diff --git a/blueprintcompiler/language/imports.py b/blueprintcompiler/language/imports.py index f0fe3df..be6a003 100644 --- a/blueprintcompiler/language/imports.py +++ b/blueprintcompiler/language/imports.py @@ -24,8 +24,12 @@ from .common import * class GtkDirective(AstNode): grammar = Statement( - Match("using").err("File must start with a \"using Gtk\" directive (e.g. `using Gtk 4.0;`)"), - Match("Gtk").err("File must start with a \"using Gtk\" directive (e.g. `using Gtk 4.0;`)"), + Match("using").err( + 'File must start with a "using Gtk" directive (e.g. `using Gtk 4.0;`)' + ), + Match("Gtk").err( + 'File must start with a "using Gtk" directive (e.g. `using Gtk 4.0;`)' + ), UseNumberText("version").expected("a version number for GTK"), ) @@ -35,7 +39,9 @@ class GtkDirective(AstNode): if version not in ["4.0"]: err = CompileError("Only GTK 4 is supported") if version and version.startswith("4"): - err.hint("Expected the GIR version, not an exact version number. Use 'using Gtk 4.0;'.") + err.hint( + "Expected the GIR version, not an exact version number. Use 'using Gtk 4.0;'." + ) else: err.hint("Expected 'using Gtk 4.0;'") raise err @@ -51,7 +57,6 @@ class GtkDirective(AstNode): hints=e.hints, ) - @property def gir_namespace(self): # validate the GTK version first to make sure the more specific error diff --git a/blueprintcompiler/language/response_id.py b/blueprintcompiler/language/response_id.py index 745c73f..073173a 100644 --- a/blueprintcompiler/language/response_id.py +++ b/blueprintcompiler/language/response_id.py @@ -26,21 +26,13 @@ from .common import * class ResponseId(AstNode): """Response ID of action widget.""" - ALLOWED_PARENTS: T.List[T.Tuple[str, str]] = [ - ("Gtk", "Dialog"), - ("Gtk", "InfoBar") - ] + ALLOWED_PARENTS: T.List[T.Tuple[str, str]] = [("Gtk", "Dialog"), ("Gtk", "InfoBar")] grammar = [ Keyword("response"), "=", - AnyOf( - UseIdent("response_id"), - UseNumber("response_id") - ), - Optional([ - Keyword("default"), UseLiteral("is_default", True) - ]) + AnyOf(UseIdent("response_id"), UseNumber("response_id")), + Optional([Keyword("default"), UseLiteral("is_default", True)]), ] @validate() @@ -91,18 +83,15 @@ class ResponseId(AstNode): if isinstance(response, int): if response < 0: - raise CompileError( - "Numeric response type can't be negative") + raise CompileError("Numeric response type can't be negative") elif isinstance(response, float): raise CompileError( - "Response type must be GtkResponseType member or integer," - " not float" + "Response type must be GtkResponseType member or integer," " not float" ) else: responses = gir.get_type("ResponseType", "Gtk").members.keys() if response not in responses: - raise CompileError( - f"Response type \"{response}\" doesn't exist") + raise CompileError(f'Response type "{response}" doesn\'t exist') @validate("default") def no_multiple_default(self) -> None: @@ -135,4 +124,3 @@ class ResponseId(AstNode): _object: Object = self.parent.children[Object][0] return _object.tokens["id"] - diff --git a/blueprintcompiler/language/types.py b/blueprintcompiler/language/types.py index 8f39cbf..0987262 100644 --- a/blueprintcompiler/language/types.py +++ b/blueprintcompiler/language/types.py @@ -41,7 +41,9 @@ class TypeName(AstNode): @validate("class_name") def type_exists(self): if not self.tokens["ignore_gir"] and self.gir_ns is not None: - self.root.gir.validate_type(self.tokens["class_name"], self.tokens["namespace"]) + self.root.gir.validate_type( + self.tokens["class_name"], self.tokens["namespace"] + ) @validate("namespace") def gir_ns_exists(self): @@ -56,7 +58,9 @@ class TypeName(AstNode): @property def gir_type(self) -> T.Optional[gir.Class]: if self.tokens["class_name"] and not self.tokens["ignore_gir"]: - return self.root.gir.get_type(self.tokens["class_name"], self.tokens["namespace"]) + return self.root.gir.get_type( + self.tokens["class_name"], self.tokens["namespace"] + ) return None @property @@ -82,7 +86,9 @@ class ClassName(TypeName): def gir_class_exists(self): if self.gir_type is not None and not isinstance(self.gir_type, Class): if isinstance(self.gir_type, Interface): - raise CompileError(f"{self.gir_type.full_name} is an interface, not a class") + raise CompileError( + f"{self.gir_type.full_name} is an interface, not a class" + ) else: raise CompileError(f"{self.gir_type.full_name} is not a class") @@ -93,6 +99,5 @@ class ConcreteClassName(ClassName): if isinstance(self.gir_type, Class) and self.gir_type.abstract: raise CompileError( 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}?"] + hints=[f"did you mean to use a subclass of {self.gir_type.full_name}?"], ) - diff --git a/blueprintcompiler/language/ui.py b/blueprintcompiler/language/ui.py index 5a9c4fb..c277c56 100644 --- a/blueprintcompiler/language/ui.py +++ b/blueprintcompiler/language/ui.py @@ -27,16 +27,19 @@ from .common import * class UI(AstNode): - """ The AST node for the entire file """ + """The AST node for the entire file""" grammar = [ GtkDirective, ZeroOrMore(Import), - Until(AnyOf( - Template, - menu, - Object, - ), Eof()), + Until( + AnyOf( + Template, + menu, + Object, + ), + Eof(), + ), ] @property @@ -61,11 +64,13 @@ class UI(AstNode): return gir_ctx - @property def objects_by_id(self): - return { obj.tokens["id"]: obj for obj in self.iterate_children_recursive() if obj.tokens["id"] is not None } - + return { + obj.tokens["id"]: obj + for obj in self.iterate_children_recursive() + if obj.tokens["id"] is not None + } @validate() def gir_errors(self): @@ -74,7 +79,6 @@ class UI(AstNode): if len(self._gir_errors): raise MultipleErrors(self._gir_errors) - @validate() def unique_ids(self): passed = {} @@ -84,5 +88,7 @@ class UI(AstNode): if obj.tokens["id"] in passed: token = obj.group.tokens["id"] - raise CompileError(f"Duplicate object ID '{obj.tokens['id']}'", token.start, token.end) + raise CompileError( + f"Duplicate object ID '{obj.tokens['id']}'", token.start, token.end + ) passed[obj.tokens["id"]] = obj diff --git a/blueprintcompiler/language/values.py b/blueprintcompiler/language/values.py index 2a889ec..db28490 100644 --- a/blueprintcompiler/language/values.py +++ b/blueprintcompiler/language/values.py @@ -84,13 +84,21 @@ class QuotedValue(Value): @validate() def validate_for_type(self): type = self.parent.value_type - if isinstance(type, gir.IntType) or isinstance(type, gir.UIntType) or isinstance(type, gir.FloatType): + if ( + isinstance(type, gir.IntType) + or isinstance(type, gir.UIntType) + or isinstance(type, gir.FloatType) + ): raise CompileError(f"Cannot convert string to number") elif isinstance(type, gir.StringType): pass - elif isinstance(type, gir.Class) or isinstance(type, gir.Interface) or isinstance(type, gir.Boxed): + elif ( + isinstance(type, gir.Class) + or isinstance(type, gir.Interface) + or isinstance(type, gir.Boxed) + ): parseable_types = [ "Gdk.Paintable", "Gdk.Texture", @@ -106,8 +114,12 @@ class QuotedValue(Value): if type.full_name not in parseable_types: hints = [] if isinstance(type, gir.TypeType): - hints.append(f"use the typeof operator: 'typeof({self.tokens('value')})'") - raise CompileError(f"Cannot convert string to {type.full_name}", hints=hints) + hints.append( + f"use the typeof operator: 'typeof({self.tokens('value')})'" + ) + raise CompileError( + f"Cannot convert string to {type.full_name}", hints=hints + ) elif type is not None: raise CompileError(f"Cannot convert string to {type.full_name}") @@ -127,7 +139,9 @@ class NumberValue(Value): try: int(self.tokens["value"]) except: - raise CompileError(f"Cannot convert {self.group.tokens['value']} to integer") + raise CompileError( + f"Cannot convert {self.group.tokens['value']} to integer" + ) elif isinstance(type, gir.UIntType): try: @@ -135,13 +149,17 @@ class NumberValue(Value): if int(self.tokens["value"]) < 0: raise Exception() except: - raise CompileError(f"Cannot convert {self.group.tokens['value']} to unsigned integer") + raise CompileError( + f"Cannot convert {self.group.tokens['value']} to unsigned integer" + ) elif isinstance(type, gir.FloatType): try: float(self.tokens["value"]) except: - raise CompileError(f"Cannot convert {self.group.tokens['value']} to float") + raise CompileError( + f"Cannot convert {self.group.tokens['value']} to float" + ) elif type is not None: raise CompileError(f"Cannot convert number to {type.full_name}") @@ -164,7 +182,7 @@ class Flag(AstNode): if isinstance(type, gir.Bitfield) and self.tokens["value"] not in 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()), + did_you_mean=(self.tokens["value"], type.members.keys()), ) @@ -189,14 +207,14 @@ class IdentValue(Value): if self.tokens["value"] not in 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()), + did_you_mean=(self.tokens["value"], 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"]), + did_you_mean=(self.tokens["value"], ["true", "false"]), ) elif type is not None: @@ -204,14 +222,13 @@ class IdentValue(Value): 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()), + did_you_mean=(self.tokens["value"], self.root.objects_by_id.keys()), ) elif 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}" ) - @docs() def docs(self): type = self.parent.value_type @@ -223,9 +240,7 @@ class IdentValue(Value): elif isinstance(type, gir.GirNode): return type.doc - 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) - diff --git a/blueprintcompiler/lsp.py b/blueprintcompiler/lsp.py index 6e43e3b..26a519e 100644 --- a/blueprintcompiler/lsp.py +++ b/blueprintcompiler/lsp.py @@ -35,6 +35,7 @@ def command(json_method): def decorator(func): func._json_method = json_method return func + return decorator @@ -50,8 +51,16 @@ class OpenFile: def apply_changes(self, changes): for change in changes: - start = utils.pos_to_idx(change["range"]["start"]["line"], change["range"]["start"]["character"], self.text) - end = utils.pos_to_idx(change["range"]["end"]["line"], change["range"]["end"]["character"], self.text) + start = utils.pos_to_idx( + change["range"]["start"]["line"], + change["range"]["start"]["character"], + self.text, + ) + end = utils.pos_to_idx( + change["range"]["end"]["line"], + change["range"]["end"]["character"], + self.text, + ) self.text = self.text[:start] + change["text"] + self.text[end:] self._update() @@ -69,16 +78,17 @@ class OpenFile: except CompileError as e: self.diagnostics.append(e) - def calc_semantic_tokens(self) -> T.List[int]: tokens = list(self.ast.get_semantic_tokens()) token_lists = [ [ - *utils.idx_to_pos(token.start, self.text), # line and column - token.end - token.start, # length + *utils.idx_to_pos(token.start, self.text), # line and column + token.end - token.start, # length token.type, - 0, # token modifiers - ] for token in tokens] + 0, # token modifiers + ] + for token in tokens + ] # convert line, column numbers to deltas for i, token_list in enumerate(token_lists[1:]): @@ -125,53 +135,60 @@ class LanguageServer: except Exception as e: printerr(traceback.format_exc()) - def _send(self, data): data["jsonrpc"] = "2.0" line = json.dumps(data, separators=(",", ":")) + "\r\n" printerr("output: " + line) - sys.stdout.write(f"Content-Length: {len(line.encode())}\r\nContent-Type: application/vscode-jsonrpc; charset=utf-8\r\n\r\n{line}") + sys.stdout.write( + f"Content-Length: {len(line.encode())}\r\nContent-Type: application/vscode-jsonrpc; charset=utf-8\r\n\r\n{line}" + ) sys.stdout.flush() def _send_response(self, id, result): - self._send({ - "id": id, - "result": result, - }) + self._send( + { + "id": id, + "result": result, + } + ) def _send_notification(self, method, params): - self._send({ - "method": method, - "params": params, - }) - + self._send( + { + "method": method, + "params": params, + } + ) @command("initialize") def initialize(self, id, params): from . import main self.client_capabilities = params.get("capabilities") - self._send_response(id, { - "capabilities": { - "textDocumentSync": { - "openClose": True, - "change": TextDocumentSyncKind.Incremental, - }, - "semanticTokensProvider": { - "legend": { - "tokenTypes": ["enumMember"], + self._send_response( + id, + { + "capabilities": { + "textDocumentSync": { + "openClose": True, + "change": TextDocumentSyncKind.Incremental, }, - "full": True, + "semanticTokensProvider": { + "legend": { + "tokenTypes": ["enumMember"], + }, + "full": True, + }, + "completionProvider": {}, + "codeActionProvider": {}, + "hoverProvider": True, + }, + "serverInfo": { + "name": "Blueprint", + "version": main.VERSION, }, - "completionProvider": {}, - "codeActionProvider": {}, - "hoverProvider": True, }, - "serverInfo": { - "name": "Blueprint", - "version": main.VERSION, - }, - }) + ) @command("textDocument/didOpen") def didOpen(self, id, params): @@ -198,14 +215,23 @@ class LanguageServer: @command("textDocument/hover") def hover(self, id, params): open_file = self._open_files[params["textDocument"]["uri"]] - docs = open_file.ast and open_file.ast.get_docs(utils.pos_to_idx(params["position"]["line"], params["position"]["character"], open_file.text)) + docs = open_file.ast and open_file.ast.get_docs( + utils.pos_to_idx( + params["position"]["line"], + params["position"]["character"], + open_file.text, + ) + ) if docs: - self._send_response(id, { - "contents": { - "kind": "markdown", - "value": docs, - } - }) + self._send_response( + id, + { + "contents": { + "kind": "markdown", + "value": docs, + } + }, + ) else: self._send_response(id, None) @@ -217,40 +243,59 @@ class LanguageServer: self._send_response(id, []) return - idx = utils.pos_to_idx(params["position"]["line"], params["position"]["character"], open_file.text) + idx = utils.pos_to_idx( + params["position"]["line"], params["position"]["character"], open_file.text + ) completions = complete(open_file.ast, open_file.tokens, idx) - self._send_response(id, [completion.to_json(True) for completion in completions]) - + self._send_response( + id, [completion.to_json(True) for completion in completions] + ) @command("textDocument/semanticTokens/full") def semantic_tokens(self, id, params): open_file = self._open_files[params["textDocument"]["uri"]] - self._send_response(id, { - "data": open_file.calc_semantic_tokens(), - }) - + self._send_response( + id, + { + "data": open_file.calc_semantic_tokens(), + }, + ) @command("textDocument/codeAction") def code_actions(self, id, params): open_file = self._open_files[params["textDocument"]["uri"]] - range_start = utils.pos_to_idx(params["range"]["start"]["line"], params["range"]["start"]["character"], open_file.text) - range_end = utils.pos_to_idx(params["range"]["end"]["line"], params["range"]["end"]["character"], open_file.text) + range_start = utils.pos_to_idx( + params["range"]["start"]["line"], + params["range"]["start"]["character"], + open_file.text, + ) + range_end = utils.pos_to_idx( + params["range"]["end"]["line"], + params["range"]["end"]["character"], + open_file.text, + ) actions = [ { "title": action.title, "kind": "quickfix", - "diagnostics": [self._create_diagnostic(open_file.text, open_file.uri, diagnostic)], + "diagnostics": [ + self._create_diagnostic(open_file.text, open_file.uri, diagnostic) + ], "edit": { "changes": { - open_file.uri: [{ - "range": utils.idxs_to_range(diagnostic.start, diagnostic.end, open_file.text), - "newText": action.replace_with - }] + open_file.uri: [ + { + "range": utils.idxs_to_range( + diagnostic.start, diagnostic.end, open_file.text + ), + "newText": action.replace_with, + } + ] } - } + }, } for diagnostic in open_file.diagnostics if not (diagnostic.end < range_start or diagnostic.start > range_end) @@ -259,23 +304,30 @@ class LanguageServer: self._send_response(id, actions) - def _send_file_updates(self, open_file: OpenFile): - self._send_notification("textDocument/publishDiagnostics", { - "uri": open_file.uri, - "diagnostics": [self._create_diagnostic(open_file.text, open_file.uri, err) for err in open_file.diagnostics], - }) + self._send_notification( + "textDocument/publishDiagnostics", + { + "uri": open_file.uri, + "diagnostics": [ + self._create_diagnostic(open_file.text, open_file.uri, err) + for err in open_file.diagnostics + ], + }, + ) def _create_diagnostic(self, text, uri, err): message = err.message for hint in err.hints: - message += '\nhint: ' + hint + message += "\nhint: " + hint result = { "range": utils.idxs_to_range(err.start, err.end, text), "message": message, - "severity": DiagnosticSeverity.Warning if isinstance(err, CompileWarning) else DiagnosticSeverity.Error, + "severity": DiagnosticSeverity.Warning + if isinstance(err, CompileWarning) + else DiagnosticSeverity.Error, } if len(err.references) > 0: @@ -285,7 +337,7 @@ class LanguageServer: "uri": uri, "range": utils.idxs_to_range(ref.start, ref.end, text), }, - "message": ref.message + "message": ref.message, } for ref in err.references ] @@ -297,4 +349,3 @@ for name in dir(LanguageServer): item = getattr(LanguageServer, name) if callable(item) and hasattr(item, "_json_method"): LanguageServer.commands[item._json_method] = item - diff --git a/blueprintcompiler/lsp_utils.py b/blueprintcompiler/lsp_utils.py index 5664ab6..5e4ef89 100644 --- a/blueprintcompiler/lsp_utils.py +++ b/blueprintcompiler/lsp_utils.py @@ -31,13 +31,16 @@ class TextDocumentSyncKind(enum.IntEnum): Full = 1 Incremental = 2 + class CompletionItemTag(enum.IntEnum): Deprecated = 1 + class InsertTextFormat(enum.IntEnum): PlainText = 1 Snippet = 2 + class CompletionItemKind(enum.IntEnum): Text = 1 Method = 2 @@ -91,12 +94,14 @@ class Completion: "documentation": { "kind": "markdown", "value": self.docs, - } if self.docs else None, + } + if self.docs + else None, "deprecated": self.deprecated, "insertText": insert_text, "insertTextFormat": insert_text_format, } - return { k: v for k, v in result.items() if v is not None } + return {k: v for k, v in result.items() if v is not None} class SemanticTokenType(enum.IntEnum): @@ -110,7 +115,6 @@ class DiagnosticSeverity(enum.IntEnum): Hint = 4 - @dataclass class SemanticToken: start: int diff --git a/blueprintcompiler/main.py b/blueprintcompiler/main.py index 124654d..4e4d378 100644 --- a/blueprintcompiler/main.py +++ b/blueprintcompiler/main.py @@ -30,24 +30,41 @@ from .outputs import XmlOutput VERSION = "uninstalled" LIBDIR = None + class BlueprintApp: def main(self): self.parser = argparse.ArgumentParser() self.subparsers = self.parser.add_subparsers(metavar="command") self.parser.set_defaults(func=self.cmd_help) - compile = self.add_subcommand("compile", "Compile blueprint files", self.cmd_compile) + compile = self.add_subcommand( + "compile", "Compile blueprint files", self.cmd_compile + ) compile.add_argument("--output", dest="output", default="-") - compile.add_argument("input", metavar="filename", default=sys.stdin, type=argparse.FileType('r')) + compile.add_argument( + "input", metavar="filename", default=sys.stdin, type=argparse.FileType("r") + ) - batch_compile = self.add_subcommand("batch-compile", "Compile many blueprint files at once", self.cmd_batch_compile) + batch_compile = self.add_subcommand( + "batch-compile", + "Compile many blueprint files at once", + self.cmd_batch_compile, + ) batch_compile.add_argument("output_dir", metavar="output-dir") batch_compile.add_argument("input_dir", metavar="input-dir") - batch_compile.add_argument("inputs", nargs="+", metavar="filenames", default=sys.stdin, type=argparse.FileType('r')) + batch_compile.add_argument( + "inputs", + nargs="+", + metavar="filenames", + default=sys.stdin, + type=argparse.FileType("r"), + ) port = self.add_subcommand("port", "Interactive porting tool", self.cmd_port) - lsp = self.add_subcommand("lsp", "Run the language server (for internal use by IDEs)", self.cmd_lsp) + lsp = self.add_subcommand( + "lsp", "Run the language server (for internal use by IDEs)", self.cmd_lsp + ) self.add_subcommand("help", "Show this message", self.cmd_help) @@ -65,17 +82,14 @@ class BlueprintApp: except: report_bug() - def add_subcommand(self, name, help, func): parser = self.subparsers.add_parser(name, help=help) parser.set_defaults(func=func) return parser - def cmd_help(self, opts): self.parser.print_help() - def cmd_compile(self, opts): data = opts.input.read() try: @@ -93,14 +107,15 @@ class BlueprintApp: e.pretty_print(opts.input.name, data) sys.exit(1) - def cmd_batch_compile(self, opts): for file in opts.inputs: data = file.read() try: if not os.path.commonpath([file.name, opts.input_dir]): - print(f"{Colors.RED}{Colors.BOLD}error: input file '{file.name}' is not in input directory '{opts.input_dir}'{Colors.CLEAR}") + print( + f"{Colors.RED}{Colors.BOLD}error: input file '{file.name}' is not in input directory '{opts.input_dir}'{Colors.CLEAR}" + ) sys.exit(1) xml, warnings = self._compile(data) @@ -111,9 +126,8 @@ class BlueprintApp: path = os.path.join( opts.output_dir, os.path.relpath( - os.path.splitext(file.name)[0] + ".ui", - opts.input_dir - ) + os.path.splitext(file.name)[0] + ".ui", opts.input_dir + ), ) os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, "w") as file: @@ -122,16 +136,13 @@ class BlueprintApp: e.pretty_print(file.name, data) sys.exit(1) - def cmd_lsp(self, opts): langserv = LanguageServer() langserv.run() - def cmd_port(self, opts): interactive_port.run(opts) - def _compile(self, data: str) -> T.Tuple[str, T.List[PrintableError]]: tokens = tokenizer.tokenize(data) ast, errors, warnings = parser.parse(tokens) diff --git a/blueprintcompiler/outputs/__init__.py b/blueprintcompiler/outputs/__init__.py index e3054a3..6cdb07b 100644 --- a/blueprintcompiler/outputs/__init__.py +++ b/blueprintcompiler/outputs/__init__.py @@ -1,7 +1,9 @@ from ..language import UI + class OutputFormat: def emit(self, ui: UI) -> str: raise NotImplementedError() + from .xml import XmlOutput diff --git a/blueprintcompiler/parse_tree.py b/blueprintcompiler/parse_tree.py index 0e2644b..6634994 100644 --- a/blueprintcompiler/parse_tree.py +++ b/blueprintcompiler/parse_tree.py @@ -24,7 +24,13 @@ import typing as T from collections import defaultdict from enum import Enum -from .errors import assert_true, CompilerBugError, CompileError, CompileWarning, UnexpectedTokenError +from .errors import ( + assert_true, + CompilerBugError, + CompileError, + CompileWarning, + UnexpectedTokenError, +) from .tokenizer import Token, TokenType @@ -32,15 +38,15 @@ SKIP_TOKENS = [TokenType.COMMENT, TokenType.WHITESPACE] class ParseResult(Enum): - """ Represents the result of parsing. The extra EMPTY result is necessary + """Represents the result of parsing. The extra EMPTY result is necessary to avoid freezing the parser: imagine a ZeroOrMore node containing a node that can match empty. It will repeatedly match empty and never advance the parser. So, ZeroOrMore stops when a failed *or empty* match is - made. """ + made.""" SUCCESS = 0 FAILURE = 1 - EMPTY = 2 + EMPTY = 2 def matched(self): return self == ParseResult.SUCCESS @@ -53,10 +59,10 @@ class ParseResult(Enum): class ParseGroup: - """ A matching group. Match groups have an AST type, children grouped by + """A matching group. Match groups have an AST type, children grouped by type, and key=value pairs. At the end of parsing, the match groups will be converted to AST nodes by passing the children and key=value pairs to - the AST node constructor. """ + the AST node constructor.""" def __init__(self, ast_type, start: int): self.ast_type = ast_type @@ -77,23 +83,27 @@ class ParseGroup: self.tokens[key] = token def to_ast(self): - """ Creates an AST node from the match group. """ + """Creates an AST node from the match group.""" children = [child.to_ast() for child in self.children] try: return self.ast_type(self, children, self.keys, incomplete=self.incomplete) except TypeError as e: - raise CompilerBugError(f"Failed to construct ast.{self.ast_type.__name__} from ParseGroup. See the previous stacktrace.") + raise CompilerBugError( + f"Failed to construct ast.{self.ast_type.__name__} from ParseGroup. See the previous stacktrace." + ) def __str__(self): result = str(self.ast_type.__name__) result += "".join([f"\n{key}: {val}" for key, val in self.keys.items()]) + "\n" - result += "\n".join([str(child) for children in self.children.values() for child in children]) + result += "\n".join( + [str(child) for children in self.children.values() for child in children] + ) return result.replace("\n", "\n ") class ParseContext: - """ Contains the state of the parser. """ + """Contains the state of the parser.""" def __init__(self, tokens, index=0): self.tokens = list(tokens) @@ -110,12 +120,11 @@ class ParseContext: self.errors = [] self.warnings = [] - def create_child(self): - """ Creates a new ParseContext at this context's position. The new + """Creates a new ParseContext at this context's position. The new context will be used to parse one node. If parsing is successful, the new context will be applied to "self". If parsing fails, the new - context will be discarded. """ + context will be discarded.""" ctx = ParseContext(self.tokens, self.index) ctx.errors = self.errors ctx.warnings = self.warnings @@ -123,7 +132,7 @@ class ParseContext: return ctx def apply_child(self, other): - """ Applies a child context to this context. """ + """Applies a child context to this context.""" if other.group is not None: # If the other context had a match group, collect all the matched @@ -150,43 +159,44 @@ class ParseContext: elif other.last_group: self.last_group = other.last_group - def start_group(self, ast_type): - """ Sets this context to have its own match group. """ + """Sets this context to have its own match group.""" assert_true(self.group is None) self.group = ParseGroup(ast_type, self.tokens[self.index].start) def set_group_val(self, key, value, token): - """ Sets a matched key=value pair on the current match group. """ + """Sets a matched key=value pair on the current match group.""" assert_true(key not in self.group_keys) self.group_keys[key] = (value, token) def set_group_incomplete(self): - """ Marks the current match group as incomplete (it could not be fully - parsed, but the parser recovered). """ + """Marks the current match group as incomplete (it could not be fully + parsed, but the parser recovered).""" self.group_incomplete = True - def skip(self): - """ Skips whitespace and comments. """ - while self.index < len(self.tokens) and self.tokens[self.index].type in SKIP_TOKENS: + """Skips whitespace and comments.""" + while ( + self.index < len(self.tokens) + and self.tokens[self.index].type in SKIP_TOKENS + ): self.index += 1 def next_token(self) -> Token: - """ Advances the token iterator and returns the next token. """ + """Advances the token iterator and returns the next token.""" self.skip() token = self.tokens[self.index] self.index += 1 return token def peek_token(self) -> Token: - """ Returns the next token without advancing the iterator. """ + """Returns the next token without advancing the iterator.""" self.skip() token = self.tokens[self.index] return token def skip_unexpected_token(self): - """ Skips a token and logs an "unexpected token" error. """ + """Skips a token and logs an "unexpected token" error.""" self.skip() start = self.tokens[self.index].start @@ -194,9 +204,11 @@ class ParseContext: self.skip() end = self.tokens[self.index - 1].end - if (len(self.errors) - and isinstance((err := self.errors[-1]), UnexpectedTokenError) - and err.end == start): + if ( + len(self.errors) + and isinstance((err := self.errors[-1]), UnexpectedTokenError) + and err.end == start + ): err.end = end else: self.errors.append(UnexpectedTokenError(start, end)) @@ -206,10 +218,10 @@ class ParseContext: class ParseNode: - """ Base class for the nodes in the parser tree. """ + """Base class for the nodes in the parser tree.""" def parse(self, ctx: ParseContext) -> ParseResult: - """ Attempts to match the ParseNode at the context's current location. """ + """Attempts to match the ParseNode at the context's current location.""" start_idx = ctx.index inner_ctx = ctx.create_child() @@ -226,22 +238,22 @@ class ParseNode: raise NotImplementedError() def err(self, message): - """ Causes this ParseNode to raise an exception if it fails to parse. + """Causes this ParseNode to raise an exception if it fails to parse. This prevents the parser from backtracking, so you should understand - what it does and how the parser works before using it. """ + what it does and how the parser works before using it.""" return Err(self, message) def expected(self, expect): - """ Convenience method for err(). """ + """Convenience method for err().""" return self.err("Expected " + expect) def warn(self, message): - """ Causes this ParseNode to emit a warning if it parses successfully. """ + """Causes this ParseNode to emit a warning if it parses successfully.""" return Warning(self, message) class Err(ParseNode): - """ ParseNode that emits a compile error if it fails to parse. """ + """ParseNode that emits a compile error if it fails to parse.""" def __init__(self, child, message): self.child = to_parse_node(child) @@ -260,7 +272,7 @@ class Err(ParseNode): class Warning(ParseNode): - """ ParseNode that emits a compile warning if it parses successfully. """ + """ParseNode that emits a compile warning if it parses successfully.""" def __init__(self, child, message): self.child = to_parse_node(child) @@ -272,12 +284,14 @@ class Warning(ParseNode): if self.child.parse(ctx).succeeded(): start_token = ctx.tokens[start_idx] end_token = ctx.tokens[ctx.index] - ctx.warnings.append(CompileWarning(self.message, start_token.start, end_token.end)) + ctx.warnings.append( + CompileWarning(self.message, start_token.start, end_token.end) + ) return True class Fail(ParseNode): - """ ParseNode that emits a compile error if it parses successfully. """ + """ParseNode that emits a compile error if it parses successfully.""" def __init__(self, child, message): self.child = to_parse_node(child) @@ -296,7 +310,8 @@ class Fail(ParseNode): class Group(ParseNode): - """ ParseNode that creates a match group. """ + """ParseNode that creates a match group.""" + def __init__(self, ast_type, child): self.ast_type = ast_type self.child = to_parse_node(child) @@ -308,7 +323,8 @@ class Group(ParseNode): class Sequence(ParseNode): - """ ParseNode that attempts to match all of its children in sequence. """ + """ParseNode that attempts to match all of its children in sequence.""" + def __init__(self, *children): self.children = [to_parse_node(child) for child in children] @@ -320,8 +336,9 @@ class Sequence(ParseNode): class Statement(ParseNode): - """ ParseNode that attempts to match all of its children in sequence. If any - child raises an error, the error will be logged but parsing will continue. """ + """ParseNode that attempts to match all of its children in sequence. If any + child raises an error, the error will be logged but parsing will continue.""" + def __init__(self, *children): self.children = [to_parse_node(child) for child in children] @@ -344,14 +361,16 @@ class Statement(ParseNode): class AnyOf(ParseNode): - """ ParseNode that attempts to match exactly one of its children. Child - nodes are attempted in order. """ + """ParseNode that attempts to match exactly one of its children. Child + nodes are attempted in order.""" + def __init__(self, *children): self.children = children @property def children(self): return self._children + @children.setter def children(self, children): self._children = [to_parse_node(child) for child in children] @@ -364,9 +383,10 @@ class AnyOf(ParseNode): class Until(ParseNode): - """ ParseNode that repeats its child until a delimiting token is found. If + """ParseNode that repeats its child until a delimiting token is found. If the child does not match, one token is skipped and the match is attempted - again. """ + again.""" + def __init__(self, child, delimiter): self.child = to_parse_node(child) self.delimiter = to_parse_node(delimiter) @@ -387,13 +407,13 @@ class Until(ParseNode): class ZeroOrMore(ParseNode): - """ ParseNode that matches its child any number of times (including zero + """ParseNode that matches its child any number of times (including zero times). It cannot fail to parse. If its child raises an exception, one token - will be skipped and parsing will continue. """ + will be skipped and parsing will continue.""" + def __init__(self, child): self.child = to_parse_node(child) - def _parse(self, ctx): while True: try: @@ -405,8 +425,9 @@ class ZeroOrMore(ParseNode): class Delimited(ParseNode): - """ ParseNode that matches its first child any number of times (including zero - times) with its second child in between and optionally at the end. """ + """ParseNode that matches its first child any number of times (including zero + times) with its second child in between and optionally at the end.""" + def __init__(self, child, delimiter): self.child = to_parse_node(child) self.delimiter = to_parse_node(delimiter) @@ -418,8 +439,9 @@ class Delimited(ParseNode): class Optional(ParseNode): - """ ParseNode that matches its child zero or one times. It cannot fail to - parse. """ + """ParseNode that matches its child zero or one times. It cannot fail to + parse.""" + def __init__(self, child): self.child = to_parse_node(child) @@ -429,14 +451,16 @@ class Optional(ParseNode): class Eof(ParseNode): - """ ParseNode that matches an EOF token. """ + """ParseNode that matches an EOF token.""" + def _parse(self, ctx: ParseContext) -> bool: token = ctx.next_token() return token.type == TokenType.EOF class Match(ParseNode): - """ ParseNode that matches the given literal token. """ + """ParseNode that matches the given literal token.""" + def __init__(self, op): self.op = op @@ -445,7 +469,7 @@ class Match(ParseNode): return str(token) == self.op def expected(self, expect: T.Optional[str] = None): - """ Convenience method for err(). """ + """Convenience method for err().""" if expect is None: return self.err(f"Expected '{self.op}'") else: @@ -453,8 +477,9 @@ class Match(ParseNode): class UseIdent(ParseNode): - """ ParseNode that matches any identifier and sets it in a key=value pair on - the containing match group. """ + """ParseNode that matches any identifier and sets it in a key=value pair on + the containing match group.""" + def __init__(self, key): self.key = key @@ -468,8 +493,9 @@ class UseIdent(ParseNode): class UseNumber(ParseNode): - """ ParseNode that matches a number and sets it in a key=value pair on - the containing match group. """ + """ParseNode that matches a number and sets it in a key=value pair on + the containing match group.""" + def __init__(self, key): self.key = key @@ -486,8 +512,9 @@ class UseNumber(ParseNode): class UseNumberText(ParseNode): - """ ParseNode that matches a number, but sets its *original text* it in a - key=value pair on the containing match group. """ + """ParseNode that matches a number, but sets its *original text* it in a + key=value pair on the containing match group.""" + def __init__(self, key): self.key = key @@ -501,8 +528,9 @@ class UseNumberText(ParseNode): class UseQuoted(ParseNode): - """ ParseNode that matches a quoted string and sets it in a key=value pair - on the containing match group. """ + """ParseNode that matches a quoted string and sets it in a key=value pair + on the containing match group.""" + def __init__(self, key): self.key = key @@ -511,19 +539,22 @@ class UseQuoted(ParseNode): if token.type != TokenType.QUOTED: return False - string = (str(token)[1:-1] + string = ( + str(token)[1:-1] .replace("\\n", "\n") - .replace("\\\"", "\"") + .replace('\\"', '"') .replace("\\\\", "\\") - .replace("\\'", "\'")) + .replace("\\'", "'") + ) ctx.set_group_val(self.key, string, token) return True class UseLiteral(ParseNode): - """ ParseNode that doesn't match anything, but rather sets a static key=value + """ParseNode that doesn't match anything, but rather sets a static key=value pair on the containing group. Useful for, e.g., property and signal flags: - `Sequence(Keyword("swapped"), UseLiteral("swapped", True))` """ + `Sequence(Keyword("swapped"), UseLiteral("swapped", True))`""" + def __init__(self, key, literal): self.key = key self.literal = literal @@ -534,8 +565,9 @@ class UseLiteral(ParseNode): class Keyword(ParseNode): - """ Matches the given identifier and sets it as a named token, with the name - being the identifier itself. """ + """Matches the given identifier and sets it as a named token, with the name + being the identifier itself.""" + def __init__(self, kw): self.kw = kw self.set_token = True @@ -565,12 +597,13 @@ class Infix(ParseNode): def __lt__(self, other): return self.binding_power < other.binding_power + def __eq__(self, other): return self.binding_power == other.binding_power class Pratt(ParseNode): - """ Basic Pratt parser implementation. """ + """Basic Pratt parser implementation.""" def __init__(self, *children): self.children = children @@ -578,11 +611,14 @@ class Pratt(ParseNode): @property def children(self): return self._children + @children.setter def children(self, children): self._children = children self.prefixes = [child for child in children if isinstance(child, Prefix)] - self.infixes = sorted([child for child in children if isinstance(child, Infix)], reverse=True) + self.infixes = sorted( + [child for child in children if isinstance(child, Infix)], reverse=True + ) def _parse(self, ctx: ParseContext) -> bool: for prefix in self.prefixes: diff --git a/blueprintcompiler/parser.py b/blueprintcompiler/parser.py index 739a165..12c893a 100644 --- a/blueprintcompiler/parser.py +++ b/blueprintcompiler/parser.py @@ -25,7 +25,7 @@ from .language import OBJECT_CONTENT_HOOKS, VALUE_HOOKS, Template, UI def parse(tokens) -> T.Tuple[UI, T.Optional[MultipleErrors], T.List[PrintableError]]: - """ Parses a list of tokens into an abstract syntax tree. """ + """Parses a list of tokens into an abstract syntax tree.""" ctx = ParseContext(tokens) AnyOf(UI).parse(ctx) diff --git a/blueprintcompiler/tokenizer.py b/blueprintcompiler/tokenizer.py index 2e7fa1b..4991967 100644 --- a/blueprintcompiler/tokenizer.py +++ b/blueprintcompiler/tokenizer.py @@ -26,28 +26,28 @@ from .errors import CompileError class TokenType(Enum): - EOF = 0 - IDENT = 1 - QUOTED = 2 - NUMBER = 3 - OP = 4 - WHITESPACE = 5 - COMMENT = 6 - PUNCTUATION = 7 + EOF = 0 + IDENT = 1 + QUOTED = 2 + NUMBER = 3 + OP = 4 + WHITESPACE = 5 + COMMENT = 6 + PUNCTUATION = 7 _tokens = [ - (TokenType.IDENT, r"[A-Za-z_][\d\w\-_]*"), - (TokenType.QUOTED, r'"(\\"|[^"\n])*"'), - (TokenType.QUOTED, r"'(\\'|[^'\n])*'"), - (TokenType.NUMBER, r"0x[A-Za-z0-9_]+"), - (TokenType.NUMBER, r"[-+]?[\d_]+(\.[\d_]+)?"), - (TokenType.NUMBER, r"[-+]?\.[\d_]+"), - (TokenType.WHITESPACE, r"\s+"), - (TokenType.COMMENT, r"\/\*[\s\S]*?\*\/"), - (TokenType.COMMENT, r"\/\/[^\n]*"), - (TokenType.OP, r"<<|>>|=>|::|<|>|:=|\.|\|\||\||\+|\-|\*|=|:|/"), - (TokenType.PUNCTUATION, r"\(|\)|\{|\}|;|\[|\]|\,"), + (TokenType.IDENT, r"[A-Za-z_][\d\w\-_]*"), + (TokenType.QUOTED, r'"(\\"|[^"\n])*"'), + (TokenType.QUOTED, r"'(\\'|[^'\n])*'"), + (TokenType.NUMBER, r"0x[A-Za-z0-9_]+"), + (TokenType.NUMBER, r"[-+]?[\d_]+(\.[\d_]+)?"), + (TokenType.NUMBER, r"[-+]?\.[\d_]+"), + (TokenType.WHITESPACE, r"\s+"), + (TokenType.COMMENT, r"\/\*[\s\S]*?\*\/"), + (TokenType.COMMENT, r"\/\/[^\n]*"), + (TokenType.OP, r"<<|>>|=>|::|<|>|:=|\.|\|\||\||\+|\-|\*|=|:|/"), + (TokenType.PUNCTUATION, r"\(|\)|\{|\}|;|\[|\]|\,"), ] _TOKENS = [(type, re.compile(regex)) for (type, regex) in _tokens] @@ -60,7 +60,7 @@ class Token: self.string = string def __str__(self): - return self.string[self.start:self.end] + return self.string[self.start : self.end] def get_number(self): if self.type != TokenType.NUMBER: @@ -73,7 +73,9 @@ class Token: else: return float(string.replace("_", "")) except: - raise CompileError(f"{str(self)} is not a valid number literal", self.start, self.end) + raise CompileError( + f"{str(self)} is not a valid number literal", self.start, self.end + ) def _tokenize(ui_ml: str): @@ -90,7 +92,9 @@ def _tokenize(ui_ml: str): break if not matched: - raise CompileError("Could not determine what kind of syntax is meant here", i, i) + raise CompileError( + "Could not determine what kind of syntax is meant here", i, i + ) yield Token(TokenType.EOF, i, i, ui_ml) diff --git a/blueprintcompiler/typelib.py b/blueprintcompiler/typelib.py index 0946320..88e7b57 100644 --- a/blueprintcompiler/typelib.py +++ b/blueprintcompiler/typelib.py @@ -241,7 +241,9 @@ class Typelib: return self._typelib_file[loc:end].decode("utf-8") def _int(self, size, signed): - return int.from_bytes(self._typelib_file[self._offset:self._offset + size], sys.byteorder) + return int.from_bytes( + self._typelib_file[self._offset : self._offset + size], sys.byteorder + ) class TypelibHeader(Typelib): diff --git a/blueprintcompiler/utils.py b/blueprintcompiler/utils.py index 2d5451d..1c69fd9 100644 --- a/blueprintcompiler/utils.py +++ b/blueprintcompiler/utils.py @@ -21,15 +21,15 @@ import typing as T class Colors: - RED = '\033[91m' - GREEN = '\033[92m' - YELLOW = '\033[33m' - FAINT = '\033[2m' - BOLD = '\033[1m' - BLUE = '\033[34m' - UNDERLINE = '\033[4m' - NO_UNDERLINE = '\033[24m' - CLEAR = '\033[0m' + RED = "\033[91m" + GREEN = "\033[92m" + YELLOW = "\033[33m" + FAINT = "\033[2m" + BOLD = "\033[1m" + BLUE = "\033[34m" + UNDERLINE = "\033[4m" + NO_UNDERLINE = "\033[24m" + CLEAR = "\033[0m" def did_you_mean(word: str, options: T.List[str]) -> T.Optional[str]: @@ -56,12 +56,16 @@ def did_you_mean(word: str, options: T.List[str]) -> T.Optional[str]: cost = 1 else: cost = 2 - distances[i][j] = min(distances[i-1][j] + 2, distances[i][j-1] + 2, distances[i-1][j-1] + cost) + distances[i][j] = min( + distances[i - 1][j] + 2, + distances[i][j - 1] + 2, + distances[i - 1][j - 1] + cost, + ) - return distances[m-1][n-1] + return distances[m - 1][n - 1] distances = [(option, levenshtein(word, option)) for option in options] - closest = min(distances, key=lambda item:item[1]) + closest = min(distances, key=lambda item: item[1]) if closest[1] <= 5: return closest[0] return None @@ -75,10 +79,12 @@ def idx_to_pos(idx: int, text: str) -> T.Tuple[int, int]: col_num = len(sp[-1]) return (line_num - 1, col_num) + def pos_to_idx(line: int, col: int, text: str) -> int: lines = text.splitlines(keepends=True) return sum([len(line) for line in lines[:line]]) + col + def idxs_to_range(start: int, end: int, text: str): start_l, start_c = idx_to_pos(start, text) end_l, end_c = idx_to_pos(end, text) diff --git a/blueprintcompiler/xml_reader.py b/blueprintcompiler/xml_reader.py index 24ae5ff..c0552f5 100644 --- a/blueprintcompiler/xml_reader.py +++ b/blueprintcompiler/xml_reader.py @@ -25,11 +25,24 @@ from xml import sax # To speed up parsing, we ignore all tags except these -PARSE_GIR = set([ - "repository", "namespace", "class", "interface", "property", "glib:signal", - "include", "implements", "type", "parameter", "parameters", "enumeration", - "member", "bitfield", -]) +PARSE_GIR = set( + [ + "repository", + "namespace", + "class", + "interface", + "property", + "glib:signal", + "include", + "implements", + "type", + "parameter", + "parameters", + "enumeration", + "member", + "bitfield", + ] +) class Element: @@ -41,14 +54,10 @@ class Element: @cached_property def cdata(self): - return ''.join(self.cdata_chunks) + return "".join(self.cdata_chunks) def get_elements(self, name) -> T.List["Element"]: - return [ - child - for child in self.children - if child.tag == name - ] + return [child for child in self.children if child.tag == name] def __getitem__(self, key): return self.attrs.get(key) diff --git a/tests/fuzz.py b/tests/fuzz.py index 1ebd02d..0f6a1a7 100644 --- a/tests/fuzz.py +++ b/tests/fuzz.py @@ -7,10 +7,16 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) from blueprintcompiler import tokenizer, parser, decompiler, gir from blueprintcompiler.completions import complete -from blueprintcompiler.errors import PrintableError, MultipleErrors, CompileError, CompilerBugError +from blueprintcompiler.errors import ( + PrintableError, + MultipleErrors, + CompileError, + CompilerBugError, +) from blueprintcompiler.tokenizer import Token, TokenType, tokenize from blueprintcompiler import utils + @PythonFuzz def fuzz(buf): try: @@ -29,6 +35,7 @@ def fuzz(buf): except UnicodeDecodeError: pass + if __name__ == "__main__": # Make sure Gtk 4.0 is accessible, otherwise every test will fail on that # and nothing interesting will be tested diff --git a/tests/test_samples.py b/tests/test_samples.py index 5f0d9e5..e63c36c 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -18,7 +18,7 @@ # SPDX-License-Identifier: LGPL-3.0-or-later -import difflib # I love Python +import difflib # I love Python from pathlib import Path import traceback import unittest @@ -59,23 +59,26 @@ class TestSamples(unittest.TestCase): xml = XmlOutput() actual = xml.emit(ast) - if actual.strip() != expected.strip(): # pragma: no cover + if actual.strip() != expected.strip(): # pragma: no cover diff = difflib.unified_diff(expected.splitlines(), actual.splitlines()) print("\n".join(diff)) raise AssertionError() self.assert_docs_dont_crash(blueprint, ast) self.assert_completions_dont_crash(blueprint, ast, tokens) - except PrintableError as e: # pragma: no cover + except PrintableError as e: # pragma: no cover e.pretty_print(name + ".blp", blueprint) raise AssertionError() - def assert_sample_error(self, name): try: - with open((Path(__file__).parent / f"sample_errors/{name}.blp").resolve()) as f: + with open( + (Path(__file__).parent / f"sample_errors/{name}.blp").resolve() + ) as f: blueprint = f.read() - with open((Path(__file__).parent / f"sample_errors/{name}.err").resolve()) as f: + with open( + (Path(__file__).parent / f"sample_errors/{name}.err").resolve() + ) as f: expected = f.read() tokens = tokenizer.tokenize(blueprint) @@ -91,6 +94,7 @@ class TestSamples(unittest.TestCase): if len(warnings): raise MultipleErrors(warnings) except PrintableError as e: + def error_str(error): line, col = utils.idx_to_pos(error.start + 1, blueprint) len = error.end - error.start @@ -100,17 +104,16 @@ class TestSamples(unittest.TestCase): actual = error_str(e) elif isinstance(e, MultipleErrors): actual = "\n".join([error_str(error) for error in e.errors]) - else: # pragma: no cover + else: # pragma: no cover raise AssertionError() - if actual.strip() != expected.strip(): # pragma: no cover + if actual.strip() != expected.strip(): # pragma: no cover diff = difflib.unified_diff(expected.splitlines(), actual.splitlines()) print("\n".join(diff)) raise AssertionError() - else: # pragma: no cover + else: # pragma: no cover raise AssertionError("Expected a compiler error, but none was emitted") - def assert_decompile(self, name): try: with open((Path(__file__).parent / f"samples/{name}.blp").resolve()) as f: @@ -121,15 +124,14 @@ class TestSamples(unittest.TestCase): actual = decompiler.decompile(ui_path) - if actual.strip() != expected.strip(): # pragma: no cover + if actual.strip() != expected.strip(): # pragma: no cover diff = difflib.unified_diff(expected.splitlines(), actual.splitlines()) print("\n".join(diff)) raise AssertionError() - except PrintableError as e: # pragma: no cover + except PrintableError as e: # pragma: no cover e.pretty_print(name + ".blp", blueprint) raise AssertionError() - def test_samples(self): self.assert_sample("accessibility") self.assert_sample("action_widgets") @@ -161,7 +163,6 @@ class TestSamples(unittest.TestCase): self.assert_sample("unchecked_class") self.assert_sample("using") - def test_sample_errors(self): self.assert_sample_error("a11y_in_non_widget") self.assert_sample_error("a11y_prop_dne") @@ -209,7 +210,6 @@ class TestSamples(unittest.TestCase): self.assert_sample_error("using_invalid_namespace") self.assert_sample_error("widgets_in_non_size_group") - def test_decompiler(self): self.assert_decompile("accessibility_dec") self.assert_decompile("binding") diff --git a/tests/test_tokenizer.py b/tests/test_tokenizer.py index 1c87e50..2bca595 100644 --- a/tests/test_tokenizer.py +++ b/tests/test_tokenizer.py @@ -32,47 +32,57 @@ class TestTokenizer(unittest.TestCase): for token, (type, token_str) in zip(tokens, expect): self.assertEqual(token.type, type) self.assertEqual(str(token), token_str) - except PrintableError as e: # pragma: no cover + except PrintableError as e: # pragma: no cover e.pretty_print("", string) raise e - def test_basic(self): - self.assert_tokenize("ident(){}; \n <<+>>*/=", [ - (TokenType.IDENT, "ident"), - (TokenType.PUNCTUATION, "("), - (TokenType.PUNCTUATION, ")"), - (TokenType.PUNCTUATION, "{"), - (TokenType.PUNCTUATION, "}"), - (TokenType.PUNCTUATION, ";"), - (TokenType.WHITESPACE, " \n "), - (TokenType.OP, "<<"), - (TokenType.OP, "+"), - (TokenType.OP, ">>"), - (TokenType.OP, "*"), - (TokenType.OP, "/"), - (TokenType.OP, "="), - (TokenType.EOF, ""), - ]) + self.assert_tokenize( + "ident(){}; \n <<+>>*/=", + [ + (TokenType.IDENT, "ident"), + (TokenType.PUNCTUATION, "("), + (TokenType.PUNCTUATION, ")"), + (TokenType.PUNCTUATION, "{"), + (TokenType.PUNCTUATION, "}"), + (TokenType.PUNCTUATION, ";"), + (TokenType.WHITESPACE, " \n "), + (TokenType.OP, "<<"), + (TokenType.OP, "+"), + (TokenType.OP, ">>"), + (TokenType.OP, "*"), + (TokenType.OP, "/"), + (TokenType.OP, "="), + (TokenType.EOF, ""), + ], + ) def test_quotes(self): - self.assert_tokenize(r'"this is a \n string""this is \\another \"string\""', [ - (TokenType.QUOTED, r'"this is a \n string"'), - (TokenType.QUOTED, r'"this is \\another \"string\""'), - (TokenType.EOF, ""), - ]) + self.assert_tokenize( + r'"this is a \n string""this is \\another \"string\""', + [ + (TokenType.QUOTED, r'"this is a \n string"'), + (TokenType.QUOTED, r'"this is \\another \"string\""'), + (TokenType.EOF, ""), + ], + ) def test_comments(self): - self.assert_tokenize('/* \n \\n COMMENT /* */', [ - (TokenType.COMMENT, '/* \n \\n COMMENT /* */'), - (TokenType.EOF, ""), - ]) - self.assert_tokenize('line // comment\nline', [ - (TokenType.IDENT, 'line'), - (TokenType.WHITESPACE, ' '), - (TokenType.COMMENT, '// comment'), - (TokenType.WHITESPACE, '\n'), - (TokenType.IDENT, 'line'), - (TokenType.EOF, ""), - ]) - + self.assert_tokenize( + "/* \n \\n COMMENT /* */", + [ + (TokenType.COMMENT, "/* \n \\n COMMENT /* */"), + (TokenType.EOF, ""), + ], + ) + self.assert_tokenize( + "line // comment\nline", + [ + (TokenType.IDENT, "line"), + (TokenType.WHITESPACE, " "), + (TokenType.COMMENT, "// comment"), + (TokenType.WHITESPACE, "\n"), + (TokenType.IDENT, "line"), + (TokenType.EOF, ""), + ], + )