Add GObject Introspection integration

- Parse .gir files
- Validate class, property, and signal names
This commit is contained in:
James Westman 2021-10-22 21:14:30 -05:00
parent 2ad2f1c54a
commit e553e5db29
No known key found for this signature in database
GPG key ID: CE2DBA0ADB654EA6
10 changed files with 734 additions and 58 deletions

View file

@ -22,4 +22,4 @@
from gtkblueprinttool import main
if __name__ == "__main__":
main()
main.main()

View file

@ -18,13 +18,102 @@
# SPDX-License-Identifier: LGPL-3.0-or-later
from .errors import assert_true
from .errors import assert_true, AlreadyCaughtError, CompileError, CompilerBugError, MultipleErrors
from .gir import GirContext, get_namespace
from .utils import lazy_prop
from .xml_emitter import XmlEmitter
class Validator():
def __init__(self, func, token_name=None, end_token_name=None):
self.func = func
self.token_name = token_name
self.end_token_name = end_token_name
def __get__(self, instance, owner):
if instance is None:
return self
key = "_validation_result_" + self.func.__name__
if key + "_err" in instance.__dict__:
# If the validator has failed before, raise a generic Exception.
# We want anything that depends on this validation result to
# fail, but not report the exception twice.
raise AlreadyCaughtError()
if key not in instance.__dict__:
try:
instance.__dict__[key] = self.func(instance)
except CompileError as e:
# Mark the validator as already failed so we don't print the
# same message again
instance.__dict__[key + "_err"] = True
# This mess of code sets the error's start and end positions
# from the tokens passed to the decorator, if they have not
# already been set
if self.token_name is not None and e.start is None:
group = instance.group.tokens.get(self.token_name)
if self.end_token_name is not None and group is None:
group = instance.group.tokens[self.end_token_name]
e.start = group.start
if (self.token_name is not None or self.end_token_name is not None) and e.end is None:
e.end = instance.group.tokens[self.end_token_name or self.token_name].end
# Re-raise the exception
raise e
# Return the validation result (which other validators, or the code
# generation phase, might depend on)
return instance.__dict__[key]
def validate(*args, **kwargs):
""" Decorator for functions that validate an AST node. Exceptions raised
during validation are marked with range information from the tokens. Also
creates a cached property out of the function. """
def decorator(func):
return Validator(func, *args, **kwargs)
return decorator
class AstNode:
""" Base class for nodes in the abstract syntax tree. """
def __init__(self):
self.group = None
self.parent = None
self.child_nodes = None
@lazy_prop
def root(self):
if self.parent is None:
return self
else:
return self.parent.root
@lazy_prop
def errors(self):
return list(self._get_errors())
def _get_errors(self):
for name in dir(type(self)):
item = getattr(type(self), name)
if isinstance(item, Validator):
try:
getattr(self, name)
except AlreadyCaughtError:
pass
except CompileError as e:
yield e
for child in self.child_nodes:
yield from child._get_errors()
def generate(self) -> str:
""" Generates an XML string from the node. """
xml = XmlEmitter()
@ -40,6 +129,7 @@ class UI(AstNode):
""" The AST node for the entire file """
def __init__(self, gtk_directives=[], imports=[], objects=[], templates=[]):
super().__init__()
assert_true(len(gtk_directives) == 1)
self.gtk_directive = gtk_directives[0]
@ -47,6 +137,22 @@ class UI(AstNode):
self.objects = objects
self.templates = templates
@validate()
def gir(self):
gir = GirContext()
gir.add_namespace(self.gtk_directive.gir_namespace)
for i in self.imports:
gir.add_namespace(i.gir_namespace)
return gir
@validate()
def at_most_one_template(self):
if len(self.templates) > 1:
raise CompileError(f"Only one template may be defined per file, but this file contains {len(self.templates)}",
self.templates[1].group.start)
def emit_xml(self, xml: XmlEmitter):
xml.start_tag("interface")
self.gtk_directive.emit_xml(xml)
@ -60,8 +166,21 @@ class UI(AstNode):
class GtkDirective(AstNode):
child_type = "gtk_directives"
def __init__(self, version):
super().__init__()
self.version = version
@validate("version")
def gir_namespace(self):
if self.version in ["4.0"]:
return get_namespace("Gtk", self.version)
else:
err = CompileError("Only GTK 4 is supported")
if self.version.startswith("4"):
err.hint("Expected the GIR version, not an exact version number. Use `@gtk \"4.0\";`.")
else:
err.hint("Expected `@gtk \"4.0\";`")
raise err
def emit_xml(self, xml: XmlEmitter):
xml.put_self_closing("requires", lib="gtk", version=self.version)
@ -69,9 +188,14 @@ class GtkDirective(AstNode):
class Import(AstNode):
child_type = "imports"
def __init__(self, namespace, version):
super().__init__()
self.namespace = namespace
self.version = version
@validate("namespace", "version")
def gir_namespace(self):
return get_namespace(self.namespace, self.version)
def emit_xml(self, xml: XmlEmitter):
pass
@ -79,6 +203,7 @@ class Import(AstNode):
class Template(AstNode):
child_type = "templates"
def __init__(self, name, class_name, object_content, namespace=None):
super().__init__()
assert_true(len(object_content) == 1)
self.name = name
@ -86,10 +211,16 @@ class Template(AstNode):
self.parent_class = class_name
self.object_content = object_content[0]
@validate("namespace", "class_name")
def gir_parent(self):
return self.root.gir.get_class(self.parent_class, self.parent_namespace)
def emit_xml(self, xml: XmlEmitter):
xml.start_tag("template", **{
"class": self.name,
"parent": self.parent_namespace + self.parent_class,
"parent": self.gir_parent.glib_type_name,
})
self.object_content.emit_xml(xml)
xml.end_tag()
@ -98,6 +229,7 @@ class Template(AstNode):
class Object(AstNode):
child_type = "objects"
def __init__(self, class_name, object_content, namespace=None, id=None):
super().__init__()
assert_true(len(object_content) == 1)
self.namespace = namespace
@ -105,9 +237,13 @@ class Object(AstNode):
self.id = id
self.object_content = object_content[0]
@validate("namespace", "class_name")
def gir_class(self):
return self.root.gir.get_class(self.class_name, self.namespace)
def emit_xml(self, xml: XmlEmitter):
xml.start_tag("object", **{
"class": self.namespace + self.class_name,
"class": self.gir_class.glib_type_name,
"id": self.id,
})
self.object_content.emit_xml(xml)
@ -117,6 +253,7 @@ class Object(AstNode):
class Child(AstNode):
child_type = "children"
def __init__(self, objects, child_type=None):
super().__init__()
assert_true(len(objects) == 1)
self.object = objects[0]
self.child_type = child_type
@ -130,28 +267,62 @@ class Child(AstNode):
class ObjectContent(AstNode):
child_type = "object_content"
def __init__(self, properties=[], signals=[], children=[]):
super().__init__()
self.properties = properties
self.signals = signals
self.children = children
def emit_xml(self, xml: XmlEmitter):
for prop in self.properties:
prop.emit_xml(xml)
for signal in self.signals:
signal.emit_xml(xml)
for child in self.children:
child.emit_xml(xml)
for x in [*self.properties, *self.signals, *self.children]:
x.emit_xml(xml)
class Property(AstNode):
child_type = "properties"
def __init__(self, name, value=None, translatable=False, bind_source=None, bind_property=None):
super().__init__()
self.name = name
self.value = value
self.translatable = translatable
self.bind_source = bind_source
self.bind_property = bind_property
@validate()
def gir_property(self):
if self.gir_class is not None:
return self.gir_class.properties.get(self.name)
@validate()
def gir_class(self):
parent = self.parent.parent
if isinstance(parent, Template):
return parent.gir_parent
elif isinstance(parent, Object):
return parent.gir_class
else:
raise CompilerBugError()
@validate("name")
def property_exists(self):
if self.gir_class is None:
# Objects that we have no gir data on should not be validated
# This happens for classes defined by the app itself
return
if isinstance(self.parent.parent, Template):
# If the property is part of a template, it might be defined by
# the application and thus not in gir
return
if self.gir_property is None:
raise CompileError(
f"Class {self.gir_class.full_name} does not contain a property called {self.name}",
did_you_mean=(self.name, self.gir_class.properties.keys())
)
def emit_xml(self, xml: XmlEmitter):
props = {
"name": self.name,
@ -170,6 +341,7 @@ class Property(AstNode):
class Signal(AstNode):
child_type = "signals"
def __init__(self, name, handler, swapped=False, after=False, object=False, detail_name=None):
super().__init__()
self.name = name
self.handler = handler
self.swapped = swapped
@ -177,6 +349,42 @@ class Signal(AstNode):
self.object = object
self.detail_name = detail_name
@validate()
def gir_signal(self):
if self.gir_class is not None:
return self.gir_class.signals.get(self.name)
@validate()
def gir_class(self):
parent = self.parent.parent
if isinstance(parent, Template):
return parent.gir_parent
elif isinstance(parent, Object):
return parent.gir_class
else:
raise CompilerBugError()
@validate("name")
def signal_exists(self):
if self.gir_class is None:
# Objects that we have no gir data on should not be validated
# This happens for classes defined by the app itself
return
if isinstance(self.parent.parent, Template):
# If the signal is part of a template, it might be defined by
# the application and thus not in gir
return
if self.gir_signal is None:
print(self.gir_class.signals.keys())
raise CompileError(
f"Class {self.gir_class.full_name} does not contain a signal called {self.name}",
did_you_mean=(self.name, self.gir_class.signals.keys())
)
def emit_xml(self, xml: XmlEmitter):
name = self.name
if self.detail_name:

View file

@ -19,6 +19,7 @@
import sys, traceback
from . import utils
class _colors:
@ -37,34 +38,63 @@ class PrintableError(Exception):
class CompileError(PrintableError):
""" A PrintableError with a start/end position and optional hints """
category = "error"
def __init__(self, message, start, end=None):
def __init__(self, message, start=None, end=None, did_you_mean=None, hints=None):
super().__init__(message)
self.message = message
self.start = start
self.end = end
self.hints = hints or []
def pretty_print(self, filename, code):
sp = code[:self.start+1].splitlines(keepends=True)
if did_you_mean is not None:
self._did_you_mean(*did_you_mean)
def hint(self, hint: str):
self.hints.append(hint)
return self
def _did_you_mean(self, word: str, options: [str]):
if word.replace("_", "-") in options:
self.hint(f"use '-', not '_': `{word.replace('_', '-')}`")
return
recommend = utils.did_you_mean(word, options)
if recommend is not None:
if word.casefold() == recommend.casefold():
self.hint(f"Did you mean `{recommend}` (note the capitalization)?")
else:
self.hint(f"Did you mean `{recommend}`?")
else:
self.hint("Did you check your spelling?")
self.hint("Are your dependencies up to date?")
def line_col_from_index(self, code, index):
sp = code[:index].splitlines(keepends=True)
line_num = len(sp)
col_num = len(sp[-1])
return (line_num, col_num)
def pretty_print(self, filename, code):
line_num, col_num = self.line_col_from_index(code, self.start + 1)
line = code.splitlines(True)[line_num-1]
print(f"""{_colors.RED}{_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}
""")
{_colors.FAINT}{line_num :>4} |{_colors.CLEAR}{line.rstrip()}\n {_colors.FAINT}|{" "*(col_num-1)}^{_colors.CLEAR}""")
for hint in self.hints:
print(f"{_colors.FAINT}hint: {hint}{_colors.CLEAR}")
print()
class TokenizeError(CompileError):
def __init__(self, start):
super().__init__("Could not determine what kind of syntax is meant here", start)
class ParseError(CompileError):
pass
class AlreadyCaughtError(Exception):
""" Emitted when a validation has already failed and its error message
should not be repeated. """
class MultipleErrors(PrintableError):

249
gtkblueprinttool/gir.py Normal file
View file

@ -0,0 +1,249 @@
# gir.py
#
# Copyright 2021 James Westman <james@jwestman.net>
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: LGPL-3.0-or-later
import os, sys
from .errors import CompileError, CompilerBugError
from .utils import lazy_prop
from . import xml_reader
extra_search_paths = []
_namespace_cache = {}
_search_paths = []
xdg_data_home = os.environ.get("XDG_DATA_HOME", os.path.expanduser("~/.local/share"))
_search_paths.append(os.path.join(xdg_data_home, "gir-1.0"))
xdg_data_dirs = os.environ.get("XDG_DATA_DIRS", "/usr/share:/usr/local/share").split(":")
_search_paths += [os.path.join(dir, "gir-1.0") for dir in xdg_data_dirs]
def get_namespace(namespace, version):
filename = f"{namespace}-{version}.gir"
if filename not in _namespace_cache:
for search_path in _search_paths:
path = os.path.join(search_path, filename)
if os.path.exists(path) and os.path.isfile(path):
xml = xml_reader.parse(path, xml_reader.PARSE_GIR)
repository = Repository(xml)
_namespace_cache[filename] = repository.namespaces.get(namespace)
break
if filename not in _namespace_cache:
raise CompileError(f"Namespace `{namespace}-{version}` could not be found.")
return _namespace_cache[filename]
class GirNode:
def __init__(self, xml):
self.xml = xml
@lazy_prop
def glib_type_name(self):
return self.xml["glib:type-name"]
@lazy_prop
def name(self) -> str:
return self.xml["name"]
@lazy_prop
def available_in(self) -> str:
return self.xml.get("version")
@lazy_prop
def doc(self) -> str:
el = self.xml.find("doc")
if el is None:
return None
return el.cdata
class Property(GirNode):
pass
class Signal(GirNode):
pass
class Interface(GirNode):
def __init__(self, ns, xml: xml_reader.Element):
super().__init__(xml)
self.ns = ns
self.properties = {child["name"]: Property(child) for child in xml.get_elements("property")}
self.signals = {child["name"]: Signal(child) for child in xml.get_elements("glib:signal")}
class Class(GirNode):
def __init__(self, ns, xml: xml_reader.Element):
super().__init__(xml)
self.ns = ns
self._parent = xml["parent"]
self.implements = [impl["name"] for impl in xml.get_elements("implements")]
self.own_properties = {child["name"]: Property(child) for child in xml.get_elements("property")}
self.own_signals = {child["name"]: Signal(child) for child in xml.get_elements("glib:signal")}
@lazy_prop
def properties(self):
return { p.name: p for p in self._enum_properties() }
@lazy_prop
def signals(self):
return { s.name: s for s in self._enum_signals() }
@lazy_prop
def full_name(self):
return f"{self.ns.name}.{self.name}"
@lazy_prop
def parent(self):
if self._parent is None:
return None
return self.ns.lookup_class(self._parent)
def _enum_properties(self):
yield from self.own_properties.values()
if self.parent is not None:
yield from self.parent.properties.values()
for impl in self.implements:
yield from self.ns.lookup_interface(impl).properties.values()
def _enum_signals(self):
yield from self.own_signals.values()
if self.parent is not None:
yield from self.parent.signals.values()
for impl in self.implements:
yield from self.ns.lookup_interface(impl).signals.values()
class Namespace(GirNode):
def __init__(self, repo, xml: xml_reader.Element):
super().__init__(xml)
self.repo = repo
self.classes = { child["name"]: Class(self, child) for child in xml.get_elements("class") }
self.interfaces = { child["name"]: Interface(self, child) for child in xml.get_elements("interface") }
self.version = xml["version"]
def lookup_class(self, name: str):
if "." in name:
ns, cls = name.split(".")
return self.repo.lookup_namespace(ns).lookup_class(cls)
else:
return self.classes.get(name)
def lookup_interface(self, name: str):
if "." in name:
ns, iface = name.split(".")
return self.repo.lookup_namespace(ns).lookup_interface(iface)
else:
return self.interfaces.get(name)
def lookup_namespace(self, ns: str):
return self.repo.lookup_namespace(ns)
class Repository(GirNode):
def __init__(self, xml: xml_reader.Element):
super().__init__(xml)
self.namespaces = { child["name"]: Namespace(self, child) for child in xml.get_elements("namespace") }
try:
self.includes = { include["name"]: get_namespace(include["name"], include["version"]) for include in xml.get_elements("include") }
except:
raise CompilerBugError(f"Failed to load dependencies of {namespace}-{version}")
def lookup_namespace(self, name: str):
ns = self.namespaces.get(name)
if ns is not None:
return ns
for include in self.includes.values():
ns = include.lookup_namespace(name)
if ns is not None:
return ns
class GirContext:
def __init__(self):
self.namespaces = {}
self.incomplete = set([])
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}-{version} can't be imported because version {other.version} was imported earlier")
self.namespaces[namespace.name] = namespace
def add_incomplete(self, namespace: str):
""" Adds an "incomplete" namespace for which missing items won't cause
errors. """
self.incomplete.add(namespace)
def get_class(self, name: str, ns:str=None) -> Class:
if ns is None:
options = [namespace.classes[name]
for namespace in self.namespaces.values()
if name in namespace.classes]
if len(options) == 1:
return options[0]
elif len(options) == 0:
raise CompileError(
f"No imported namespace contains a class called {name}",
hints=[
"Did you forget to import a namespace?",
"Did you check your spelling?",
"Are your dependencies up to date?",
],
)
else:
raise CompileError(
f"Class name {name} is ambiguous",
hints=[
f"Specify the namespace, e.g. `{options[0].ns.name}.{name}`",
f"Namespaces with a class named {name}: {', '.join([cls.ns.name for cls in options])}",
],
)
else:
if ns not in self.namespaces:
raise CompileError(
f"Namespace `{ns}` was not imported.",
did_you_mean=(ns, self.namespaces.keys()),
)
if name not in self.namespaces[ns].classes:
raise CompileError(
f"Namespace {ns} does not contain a class called {name}.",
did_you_mean=(name, self.namespaces[ns].classes.keys()),
)
return self.namespaces[ns].classes[name]

View file

@ -18,9 +18,10 @@
# SPDX-License-Identifier: LGPL-3.0-or-later
import argparse, sys
import argparse, json, os, sys
from .errors import PrintableError, report_compile_error
from .errors import PrintableError, report_compile_error, MultipleErrors
from .lsp import LanguageServer
from .pipeline import Pipeline
from . import parser, tokenizer
@ -43,6 +44,8 @@ class BlueprintApp:
try:
opts = self.parser.parse_args()
opts.func(opts)
except SystemExit as e:
raise e
except:
report_compile_error()
@ -60,7 +63,12 @@ class BlueprintApp:
try:
tokens = tokenizer.tokenize(data)
ast = parser.parse(tokens)
if len(ast.errors):
raise MultipleErrors(ast.errors)
xml = ast.generate()
if opts.output == "-":
print(xml)
else:

View file

@ -22,7 +22,7 @@
from enum import Enum
from .ast import AstNode
from .errors import assert_true, CompilerBugError, CompileError, ParseError
from .errors import assert_true, CompilerBugError, CompileError
from .tokenizer import Token, TokenType
@ -57,10 +57,13 @@ class ParseGroup:
be converted to AST nodes by passing the children and key=value pairs to
the AST node constructor. """
def __init__(self, ast_type):
def __init__(self, ast_type, start: int):
self.ast_type = ast_type
self.children = {}
self.keys = {}
self.tokens = {}
self.start = start
self.end = None
def add_child(self, child):
child_type = child.ast_type.child_type
@ -68,10 +71,11 @@ class ParseGroup:
self.children[child_type] = []
self.children[child_type].append(child)
def set_val(self, key, val):
def set_val(self, key, val, token):
assert_true(key not in self.keys)
self.keys[key] = val
self.tokens[key] = token
def to_ast(self) -> AstNode:
""" Creates an AST node from the match group. """
@ -80,7 +84,12 @@ class ParseGroup:
for child_type, children in self.children.items()
}
try:
return self.ast_type(**children, **self.keys)
ast = self.ast_type(**children, **self.keys)
ast.group = self
ast.child_nodes = [c for child_type in children.values() for c in child_type]
for child in ast.child_nodes:
child.parent = ast
return ast
except TypeError as e:
raise CompilerBugError(f"Failed to construct ast.{self.ast_type.__name__} from ParseGroup. See the previous stacktrace.")
@ -113,7 +122,10 @@ class ParseContext:
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. """
return ParseContext(self.tokens, self.index)
ctx = ParseContext(self.tokens, self.index)
ctx.errors = self.errors
ctx.warnings = self.warnings
return ctx
def apply_child(self, other):
""" Applies a child context to this context. """
@ -121,10 +133,11 @@ class ParseContext:
if other.group is not None:
# If the other context had a match group, collect all the matched
# values into it and then add it to our own match group.
for key, val in other.group_keys.items():
other.group.set_val(key, val)
for key, (val, token) in other.group_keys.items():
other.group.set_val(key, val, token)
for child in other.group_children:
other.group.add_child(child)
other.group.end = other.tokens[other.index - 1].end
self.group_children.append(other.group)
else:
# If the other context had no match group of its own, collect all
@ -144,23 +157,12 @@ class ParseContext:
def start_group(self, ast_type):
""" Sets this context to have its own match group. """
assert_true(self.group is None)
self.group = ParseGroup(ast_type)
self.group = ParseGroup(ast_type, self.tokens[self.index].start)
def set_group_val(self, key, value):
def set_group_val(self, key, value, token):
""" Sets a matched key=value pair on the current match group. """
assert_true(key not in self.group_keys)
self.group_keys[key] = value
def create_parse_error(self, message):
""" Creates a ParseError identifying the current token index. """
start_idx = self.start
while self.tokens[start_idx].type in _SKIP_TOKENS:
start_idx += 1
start_token = self.tokens[start_idx]
end_token = self.tokens[self.index]
return ParseError(message, start_token.start, end_token.end)
self.group_keys[key] = (value, token)
def skip(self):
@ -224,7 +226,32 @@ class Err(ParseNode):
def _parse(self, ctx):
if self.child.parse(ctx).failed():
raise ctx.create_parse_error(self.message)
start_idx = ctx.start
while ctx.tokens[start_idx].type in _SKIP_TOKENS:
start_idx += 1
start_token = ctx.tokens[start_idx]
end_token = ctx.tokens[ctx.index]
raise CompileError(self.message, start_token.start, end_token.end)
return True
class Fail(ParseNode):
""" ParseNode that emits a compile error if it parses successfully. """
def __init__(self, child, message):
self.child = child
self.message = message
def _parse(self, ctx):
if self.child.parse(ctx).succeeded():
start_idx = ctx.start
while ctx.tokens[start_idx].type in _SKIP_TOKENS:
start_idx += 1
start_token = ctx.tokens[start_idx]
end_token = ctx.tokens[ctx.index]
raise CompileError(self.message, start_token.start, end_token.end)
return True
@ -250,6 +277,7 @@ class Group(ParseNode):
self.child = child
def _parse(self, ctx: ParseContext) -> bool:
ctx.skip()
ctx.start_group(self.ast_type)
return self.child.parse(ctx).succeeded()
@ -367,16 +395,15 @@ class UseIdent(ParseNode):
if token.type != TokenType.IDENT:
return False
ctx.set_group_val(self.key, str(token))
ctx.set_group_val(self.key, str(token), token)
return True
class UseNumber(ParseNode):
""" ParseNode that matches a number and sets it in a key=value pair on
the containing match group. """
def __init__(self, key, keep_trailing_decimal=False):
def __init__(self, key):
self.key = key
self.keep_trailing_decimal = keep_trailing_decimal
def _parse(self, ctx: ParseContext):
token = ctx.next_token()
@ -384,9 +411,9 @@ class UseNumber(ParseNode):
return False
number = token.get_number()
if not self.keep_trailing_decimal and number % 1.0 == 0:
if number % 1.0 == 0:
number = int(number)
ctx.set_group_val(self.key, number)
ctx.set_group_val(self.key, number, token)
return True
@ -405,7 +432,7 @@ class UseQuoted(ParseNode):
.replace("\\n", "\n")
.replace("\\\"", "\"")
.replace("\\\\", "\\"))
ctx.set_group_val(self.key, string)
ctx.set_group_val(self.key, string, token)
return True
@ -418,7 +445,7 @@ class UseLiteral(ParseNode):
self.literal = literal
def _parse(self, ctx: ParseContext):
ctx.set_group_val(self.key, self.literal)
ctx.set_group_val(self.key, self.literal, None)
return True

View file

@ -19,7 +19,7 @@
from . import ast
from .errors import MultipleErrors, ParseError
from .errors import MultipleErrors
from .parse_tree import *
from .tokenizer import TokenType
@ -31,7 +31,8 @@ def parse(tokens) -> ast.UI:
ast.GtkDirective,
Sequence(
Directive("gtk"),
UseNumber("version", True).expected("a version number for GTK"),
Fail(UseNumber(None), "Version number must be in quotation marks"),
UseQuoted("version").expected("a version number for GTK"),
StmtEnd().expected("`;`"),
)
)
@ -41,7 +42,8 @@ def parse(tokens) -> ast.UI:
Sequence(
Directive("import"),
UseIdent("namespace").expected("a GIR namespace"),
UseNumber("version", True).expected("a version number"),
Fail(UseNumber(None), "Version number must be in quotation marks"),
UseQuoted("version").expected("a version number"),
StmtEnd().expected("`;`"),
)
).recover()

View file

@ -21,7 +21,7 @@
import re
from enum import Enum
from .errors import TokenizeError
from .errors import CompileError
class TokenType(Enum):
@ -106,7 +106,7 @@ def _tokenize(ui_ml: str):
break
if not matched:
raise TokenizeError(i)
raise CompileError("Could not determine what kind of syntax is meant here", i)
yield Token(TokenType.EOF, i, i, ui_ml)

67
gtkblueprinttool/utils.py Normal file
View file

@ -0,0 +1,67 @@
# utils.py
#
# Copyright 2021 James Westman <james@jwestman.net>
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: LGPL-3.0-or-later
import typing as T
def lazy_prop(func):
key = "_lazy_prop_" + func.__name__
@property
def real_func(self):
if key not in self.__dict__:
self.__dict__[key] = func(self)
return self.__dict__[key]
return real_func
def did_you_mean(word: str, options: [str]) -> T.Optional[str]:
if len(options) == 0:
return None
def levenshtein(a, b):
# see https://en.wikipedia.org/wiki/Levenshtein_distance
m = len(a)
n = len(b)
distances = [[0 for j in range(n)] for i in range(m)]
for i in range(m):
distances[i][0] = i
for j in range(n):
distances[0][j] = j
for j in range(1, n):
for i in range(1, m):
cost = 0
if a[i] != b[j]:
if a[i].casefold() == b[j].casefold():
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)
return distances[m-1][n-1]
distances = [(option, levenshtein(word, option)) for option in options]
closest = min(distances, key=lambda item:item[1])
if closest[1] <= 5:
return closest[0]
return None

View file

@ -0,0 +1,85 @@
# xml_reader.py
#
# Copyright 2021 James Westman <james@jwestman.net>
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: LGPL-3.0-or-later
from collections import defaultdict
from xml import sax
from .utils import lazy_prop
PARSE_GIR = set([
"repository", "namespace", "class", "interface", "property", "glib:signal",
"include",
])
class Element:
def __init__(self, tag, attrs):
self.tag = tag
self.attrs = attrs
self.children = defaultdict(list)
self.cdata_chunks = []
@lazy_prop
def cdata(self):
return ''.join(self.cdata_chunks)
def get_elements(self, name):
return self.children.get(name, [])
def __getitem__(self, key):
return self.attrs.get(key)
class Handler(sax.handler.ContentHandler):
def __init__(self, parse_type):
self.root = None
self.stack = []
self._interesting_elements = parse_type
def startElement(self, name, attrs):
if name not in self._interesting_elements:
return
element = Element(name, attrs.copy())
if len(self.stack):
last = self.stack[-1]
last.children[name].append(element)
else:
self.root = element
self.stack.append(element)
def endElement(self, name):
if name in self._interesting_elements:
self.stack.pop()
def characters(self, content):
self.stack[-1].cdata_chunks.append(content)
def parse(filename, parse_type):
parser = sax.make_parser()
handler = Handler(parse_type)
parser.setContentHandler(handler)
parser.parse(filename)
return handler.root