Compare commits

...

8 commits

Author SHA1 Message Date
Peter Eisenmann
dc6fe66d32 Merge branch 'wip/p3732/search-paths' into 'main'
Expand gir/typelib search paths

See merge request jwestman/blueprint-compiler!183
2024-12-21 19:59:59 +00:00
Luoyayu
778a979714 lsp: Fix format of JSON-RPC content part ending with \r\n 2024-12-10 01:27:28 +00:00
James Westman
6acf0fe5a0
tests: Test deprecations separately
Libraries can add new deprecations, or the environment you're running
the tests in might have old libraries where the things we test aren't
deprecated yet. Move the deprecations test into its own module with its
own code, so it can check library versions and skip the test if it won't
work.
2024-12-09 19:06:10 -06:00
Peter Eisenmann
077610b877 gir: add search fallback paths 2024-05-21 13:46:05 +00:00
Peter Eisenmann
9429ac5f41 gir: use GLib to get gir base paths 2024-05-21 13:46:05 +00:00
Peter Eisenmann
ca4ef1db88 gir: use typelib paths for namespaces 2024-05-21 13:46:05 +00:00
Peter Eisenmann
c5949de543 gir: only initialize search paths once 2024-05-21 13:46:05 +00:00
Peter Eisenmann
f2d4643bf5 gir: batch-add user typelib search paths 2024-05-21 13:46:05 +00:00
7 changed files with 185 additions and 36 deletions

View file

@ -25,7 +25,8 @@ from functools import cached_property
import gi # type: ignore
gi.require_version("GIRepository", "2.0")
from gi.repository import GIRepository # type: ignore
gi.require_version("GLib", "2.0")
from gi.repository import GIRepository, GLib # type: ignore
from . import typelib, xml_reader
from .errors import CompileError, CompilerBugError
@ -35,19 +36,60 @@ _namespace_cache: T.Dict[str, "Namespace"] = {}
_xml_cache = {}
_user_search_paths = []
_typelib_search_paths_initalized: bool = False
_typelib_search_paths: list[str] = []
_gir_search_paths_initalized: bool = False
_gir_search_paths: list[str] = []
def add_typelib_search_path(path: str):
_user_search_paths.append(path)
def add_user_typelib_paths(paths: list):
global _user_search_paths
_user_search_paths += paths
def collect_typelib_search_paths():
global _typelib_search_paths_initalized
if _typelib_search_paths_initalized:
return
global _typelib_search_paths
_typelib_search_paths += [
*_user_search_paths,
*GIRepository.Repository.get_search_path(),
# fallback paths
"/usr/local/lib/girepository-1.0",
"/usr/local/lib64/girepository-1.0",
"/usr/lib/girepository-1.0",
"/usr/lib64/girepository-1.0",
]
_typelib_search_paths_initalized = True
def collect_gir_search_paths():
global _gir_search_paths_initalized
if _gir_search_paths_initalized:
return
base_paths = [
GLib.get_user_data_dir(),
*GLib.get_system_data_dirs(),
# fallback paths
"/usr/local/share/",
"/usr/share/",
]
global _gir_search_paths
_gir_search_paths = [os.path.join(path, "gir-1.0") for path in base_paths]
_gir_search_paths_initalized = True
def get_namespace(namespace: str, version: str) -> "Namespace":
search_paths = [*GIRepository.Repository.get_search_path(), *_user_search_paths]
collect_typelib_search_paths()
filename = f"{namespace}-{version}.typelib"
if filename not in _namespace_cache:
for search_path in search_paths:
for search_path in _typelib_search_paths:
path = os.path.join(search_path, filename)
if os.path.exists(path) and os.path.isfile(path):
@ -60,7 +102,7 @@ def get_namespace(namespace: str, version: str) -> "Namespace":
if filename not in _namespace_cache:
raise CompileError(
f"Namespace {namespace}-{version} could not be found",
hints=["search path: " + os.pathsep.join(search_paths)],
hints=["search path: " + os.pathsep.join(_typelib_search_paths)],
)
return _namespace_cache[filename]
@ -73,12 +115,8 @@ def get_available_namespaces() -> T.List[T.Tuple[str, str]]:
if len(_available_namespaces):
return _available_namespaces
search_paths: list[str] = [
*GIRepository.Repository.get_search_path(),
*_user_search_paths,
]
for search_path in search_paths:
collect_typelib_search_paths()
for search_path in _typelib_search_paths:
try:
filenames = os.listdir(search_path)
except FileNotFoundError:
@ -93,17 +131,12 @@ def get_available_namespaces() -> T.List[T.Tuple[str, str]]:
def get_xml(namespace: str, version: str):
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)
]
collect_gir_search_paths()
filename = f"{namespace}-{version}.gir"
if filename not in _xml_cache:
for search_path in search_paths:
for search_path in _gir_search_paths:
path = os.path.join(search_path, filename)
if os.path.exists(path) and os.path.isfile(path):
@ -113,7 +146,7 @@ def get_xml(namespace: str, version: str):
if filename not in _xml_cache:
raise CompileError(
f"GObject introspection file '{namespace}-{version}.gir' could not be found",
hints=["search path: " + os.pathsep.join(search_paths)],
hints=["search path: " + os.pathsep.join(_gir_search_paths)],
)
return _xml_cache[filename]

View file

@ -171,6 +171,24 @@ class LookupOp(InfixExpr):
did_you_mean=(self.property_name, self.lhs.type.properties.keys()),
)
@validate("property")
def property_deprecated(self):
if self.lhs.type is None or not (
isinstance(self.lhs.type, gir.Class)
or isinstance(self.lhs.type, gir.Interface)
):
return
if property := self.lhs.type.properties.get(self.property_name):
if property.deprecated:
hints = []
if property.deprecated_doc:
hints.append(property.deprecated_doc)
raise DeprecatedWarning(
f"{property.signature} is deprecated",
hints=hints,
)
class CastExpr(InfixExpr):
grammar = [

View file

@ -149,7 +149,7 @@ class LanguageServer:
def _send(self, data):
data["jsonrpc"] = "2.0"
line = json.dumps(data, separators=(",", ":")) + "\r\n"
line = json.dumps(data, separators=(",", ":"))
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}"

View file

@ -27,7 +27,7 @@ import typing as T
from . import formatter, interactive_port, parser, tokenizer
from .decompiler import decompile_string
from .errors import CompileError, CompilerBugError, PrintableError, report_bug
from .gir import add_typelib_search_path
from .gir import add_user_typelib_paths
from .lsp import LanguageServer
from .outputs import XmlOutput
from .utils import Colors
@ -145,8 +145,7 @@ class BlueprintApp:
def cmd_compile(self, opts):
if opts.typelib_path != None:
for typelib_path in opts.typelib_path:
add_typelib_search_path(typelib_path)
add_user_typelib_paths(opts.typelib_path)
data = opts.input.read()
try:
@ -166,8 +165,7 @@ class BlueprintApp:
def cmd_batch_compile(self, opts):
if opts.typelib_path != None:
for typelib_path in opts.typelib_path:
add_typelib_search_path(typelib_path)
add_user_typelib_paths(opts.typelib_path)
for file in opts.inputs:
data = file.read()

View file

@ -1,9 +0,0 @@
using Gtk 4.0;
Dialog {
use-header-bar: 1;
}
Window {
keys-changed => $on_window_keys_changed();
}

View file

@ -1 +0,0 @@
3,1,6,Gtk.Dialog is deprecated

110
tests/test_deprecations.py Normal file
View file

@ -0,0 +1,110 @@
# test_samples.py
#
# Copyright 2024 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 unittest
import gi
gi.require_version("Gtk", "4.0")
from gi.repository import Gtk
from blueprintcompiler import parser, tokenizer
from blueprintcompiler.errors import DeprecatedWarning, PrintableError
# Testing deprecation warnings requires special handling because libraries can add deprecations with new versions,
# causing tests to break if we're not careful.
class TestDeprecations(unittest.TestCase):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.gtkVersion = f"{Gtk.get_major_version()}.{Gtk.get_minor_version()}.{Gtk.get_micro_version()}"
def assertDeprecation(self, blueprint: str, message: str):
try:
tokens = tokenizer.tokenize(blueprint)
_ast, errors, warnings = parser.parse(tokens)
self.assertIsNone(errors)
self.assertEqual(len(warnings), 1)
self.assertIsInstance(warnings[0], DeprecatedWarning)
self.assertEqual(warnings[0].message, message)
except PrintableError as e: # pragma: no cover
e.pretty_print("<deprecations test>", blueprint)
raise AssertionError()
def test_class_deprecation(self):
if Gtk.check_version(4, 10, 0) is not None:
self.skipTest(f"Gtk.Dialog is not deprecated in GTK {self.gtkVersion}")
blueprint = """
using Gtk 4.0;
Dialog {
use-header-bar: 1;
}
"""
message = "Gtk.Dialog is deprecated"
self.assertDeprecation(blueprint, message)
def test_property_deprecation(self):
self.skipTest(
"gobject-introspection does not currently write property deprecations to the typelib. See <https://gitlab.gnome.org/GNOME/gobject-introspection/-/merge_requests/410>."
)
if Gtk.check_version(4, 4, 0) is not None:
self.skipTest(
f"Gtk.DropTarget:drop is not deprecated in GTK {self.gtkVersion}"
)
blueprint = """
using Gtk 4.0;
$MyObject {
a: bind drop_target.drop;
}
DropTarget drop_target {
}
"""
message = "Gtk.DropTarget:drop is deprecated"
self.assertDeprecation(blueprint, message)
def test_signal_deprecation(self):
if Gtk.check_version(4, 10, 0) is not None:
self.skipTest(
f"Gtk.Window::keys-changed is not deprecated in GTK {self.gtkVersion}"
)
blueprint = """
using Gtk 4.0;
Window {
keys-changed => $handler();
}
"""
message = "signal Gtk.Window::keys-changed () is deprecated"
self.assertDeprecation(blueprint, message)