diff --git a/pyproject.toml b/pyproject.toml index f8255ee6..961140d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,10 @@ authors = [ {email = "reiter.christoph@gmail.com"}, {name = "Christoph Reiter"} ] +dependencies = [ + # for @deprecated decorator + "typing_extensions>=4.5.0; python_version<'3.13'", +] classifiers = [ "Programming Language :: Python :: 3", "Intended Audience :: Developers", @@ -50,7 +54,8 @@ include = '\.pyi?$' skip = "*__pycache__*,.mypy_cache,.git,test,*.pyi" ignore-words-list = """ astroid, - inout""" + inout, + gir""" [tool.isort] force_alphabetical_sort_within_sections = true diff --git a/tools/generate.py b/tools/generate.py index 7e929043..ebb98474 100755 --- a/tools/generate.py +++ b/tools/generate.py @@ -24,6 +24,7 @@ import gi import gi._gi as GI +import gir import parse gi.require_version("GIRepository", "2.0") @@ -34,6 +35,8 @@ ObjectT = Union[ModuleType, Type[Any]] +DEPRECATION_DOCS: dict[str, str] = {} + def _object_get_props( obj: GI.ObjectInfo, @@ -339,9 +342,16 @@ def _build(parent: ObjectT, namespace: str, overrides: dict[str, str]) -> str: typings = "from typing import Any, Callable, Literal, Optional, Tuple, Type, TypeVar, Sequence" typevars: list[str] = [] - imports: list[str] = [] + imports: list[str] = [ + """ +try: + from warnings import deprecated +except ImportError: + from typing_extensions import deprecated +""" + ] if "cairo" in ns: - imports = ["import cairo"] + imports += ["import cairo"] typevars.append('_SomeSurface = TypeVar("_SomeSurface", bound=cairo.Surface)') ns.remove("cairo") @@ -507,6 +517,29 @@ def _check_override(prefix: str, name: str, overrides: dict[str, str]) -> Option return None +def _check_deprecation(obj: Any, full_name: str, default_message: str) -> str: + ret = "" + if hasattr(obj, "is_deprecated"): + if obj.is_deprecated(): + message = ( + # Currently not implemented: + # https://gitlab.gnome.org/GNOME/gobject-introspection/-/issues/80 + ( + obj.get_attribute("deprecated") + if hasattr(obj, "get_attribute") + else None + ) + or ( + DEPRECATION_DOCS[full_name] + if full_name in DEPRECATION_DOCS + else None + ) + or default_message + ) + ret += f'@deprecated("{message}")\n' + return ret + + def _gi_build_stub( parent: ObjectT, current_namespace: str, @@ -601,6 +634,12 @@ def _gi_build_stub( # Functions for name in sorted(functions): + full_name = _generate_full_name(prefix_name, name) + ret += _check_deprecation( + functions[name], + full_name, + f"This {'method' if in_class else 'function'} is deprecated", + ) override = _check_override(prefix_name, name, overrides) if override: ret += override + "\n" @@ -615,13 +654,18 @@ def _gi_build_stub( # Classes for name, obj in sorted(classes.items()): + full_name = _generate_full_name(prefix_name, name) + + if hasattr(obj, "__info__"): + ret += _check_deprecation( + obj.__info__, full_name, "This class is deprecated" + ) + override = _check_override(prefix_name, name, overrides) if override: ret += override + "\n\n" continue - full_name = _generate_full_name(prefix_name, name) - classret = _gi_build_stub( obj, current_namespace, @@ -804,13 +848,18 @@ def _gi_build_stub( # Flags for name, obj in sorted(flags.items()): + full_name = _generate_full_name(prefix_name, name) + + if hasattr(obj, "__info__"): + ret += _check_deprecation( + obj.__info__, full_name, "This class is deprecated" + ) + override = _check_override(prefix_name, name, overrides) if override: ret += override + "\n\n" continue - full_name = _generate_full_name(prefix_name, name) - if current_namespace == "GObject": if name != "GFlags": base = "GFlags" @@ -847,13 +896,18 @@ def _gi_build_stub( # Enums for name, obj in sorted(enums.items()): + full_name = _generate_full_name(prefix_name, name) + + if hasattr(obj, "__info__"): + ret += _check_deprecation( + obj.__info__, full_name, "This class is deprecated" + ) + override = _check_override(prefix_name, name, overrides) if override: ret += override + "\n\n" continue - full_name = _generate_full_name(prefix_name, name) - if current_namespace == "GObject": if name != "GEnum": base = "GEnum" @@ -940,6 +994,8 @@ def start(module: str, version: str, overrides: dict[str, str]) -> str: args = parser.parse_args() + DEPRECATION_DOCS = gir.load_gir(args.module, args.version) + if args.output: overrides: dict[str, str] = {} try: diff --git a/tools/gir.py b/tools/gir.py new file mode 100644 index 00000000..5961960d --- /dev/null +++ b/tools/gir.py @@ -0,0 +1,63 @@ +from typing import cast + +import os +import re +import sys +import xml.etree.ElementTree as ET + +import gi +from gi.repository import GLib + + +def _get_gir_path(girname: str) -> str: + searchdirs: list[str] = [] + + from_env = os.getenv("GI_GIR_PATH", "") + if from_env: + searchdirs.extend(from_env.split(os.pathsep)) + + user_data_dir = GLib.get_user_data_dir() + if user_data_dir is not None: + searchdirs.append(os.path.join(user_data_dir, "gir-1.0")) + + for path in GLib.get_system_data_dirs(): + searchdirs.append(os.path.join(path, "gir-1.0")) + + if os.name != "nt": + # For backwards compatibility, was always unconditionally added to the list. + searchdirs.append("/usr/share/gir-1.0") + + for d in searchdirs: + path = os.path.join(d, girname) + if os.path.exists(path): + return path + + sys.stderr.write(f"Couldn't find '{girname}' (search path: '{searchdirs}')\n") + sys.exit(1) + + +def load_gir(module: str, version: str) -> dict[str, str]: + deprecation_docs: dict[str, str] = {} + ns = { + "core": "http://www.gtk.org/introspection/core/1.0", + "c": "http://www.gtk.org/introspection/c/1.0", + "glib": "http://www.gtk.org/introspection/glib/1.0", + } + gir_tree = ET.parse(_get_gir_path(f"{module}-{version}.gir")) + gir_root = gir_tree.getroot() + gir_parent_map = {c: p for p in gir_tree.iter() for c in p} + + for child in gir_root.iterfind(".//core:doc-deprecated", ns): + parents: list[str] = [] + parent = gir_parent_map[child] + while True: + try: + parents.insert(0, parent.attrib["name"]) + except KeyError: + break + parent = gir_parent_map[parent] + deprecation_docs[".".join(parents[1:])] = re.sub( + " +", " ", cast(str, child.text).replace("\n", " ").replace('"', '\\"') + ) + + return deprecation_docs