diff --git a/tools/generate.py b/tools/generate.py index 6c8c89d0..2f00c448 100755 --- a/tools/generate.py +++ b/tools/generate.py @@ -99,13 +99,14 @@ def _callable_get_arguments( current_namespace: str, needed_namespaces: set[str], can_default: bool = False, -) -> Tuple[list[str], list[str], list[str]]: +) -> Tuple[list[str], list[str], list[str], list[str]]: function_args = type.get_arguments() accept_optional_args = False optional_args_name = "" dict_names: dict[int, str] = {} dict_args: dict[int, GI.ArgInfo] = {} str_args: list[str] = [] + dict_return_names: dict[int, str] = {} dict_return_args: dict[int, str] = {} skip: list[int] = [] @@ -132,6 +133,7 @@ def _callable_get_arguments( if len_arg < i: dict_names.pop(len_arg, None) dict_args.pop(len_arg, None) + dict_return_names.pop(len_arg, None) dict_return_args.pop(len_arg, None) # Need to check because user_data can be the first arg @@ -142,6 +144,7 @@ def _callable_get_arguments( arg.get_type(), current_namespace, needed_namespaces, True ) + dict_return_names[i] = arg.get_name() dict_return_args[i] = t elif direction == GI.Direction.IN or direction == GI.Direction.INOUT: dict_names[i] = arg.get_name() @@ -184,7 +187,7 @@ def _callable_get_arguments( if return_type != "None" or len(return_args) == 0: return_args.insert(0, return_type) - return (names, str_args, return_args) + return (names, str_args, return_args, list(dict_return_names.values())) class TypeInfo: @@ -295,7 +298,7 @@ def _type_to_python( if tag == tags.INTERFACE: interface = type.get_interface() if isinstance(interface, GI.CallbackInfo): - (names, args, return_args) = _callable_get_arguments( + (names, args, return_args, _) = _callable_get_arguments( interface, current_namespace, needed_namespaces ) @@ -375,8 +378,53 @@ def _generate_full_name(prefix: str, name: str) -> str: return full_name +def _normalize_string(s: str) -> str: + return re.sub(" +", " ", s) + + +def _build_function_info_doc( + fullname: str, args: list[str], return_args: list[str], return_names: list[str] +) -> str | None: + if not fullname in DOCS: + return None + + args = [a.replace("*", "") for a in args] + + doc = DOCS[fullname] + arg_doc: dict[str, str | None] = {} + for arg in args + return_names: + arg_doc[arg] = _normalize_string(DOCS.get(f"{fullname}.{arg}", "")) + + return_doc = _normalize_string(DOCS.get(f"{fullname}.$return-value", "")) + + # There are two conditions: + # 1: A return value and some / none out arguments, + # in this case the length will be different and we should ignore the first return_args + # 2: No return value, in this case the length will be equal + add_one = len(return_args) != len(return_names) + return_doc = ( + f"{return_args[0]}: {return_doc or 'Not documented'}\n " + if add_one and not (len(return_args) == 1 and return_args[0] == "None") + else None + ) + ret_args: dict[str, str] = {} + for i, arg in enumerate(return_names): + index = i + 1 if add_one else i + ret_args[arg] = return_args[index] + + return f"""{doc} + +Parameters: + {'\n '.join([f'{s}: {arg_doc[s]}' for s in args])} + +Returns: + {return_doc or ""}{'\n '.join([f'{ret_args[s]}: {arg_doc[s]}' for s in return_names])} +""" + + def _build_function_info( current_namespace: str, + fullname: str, name: str, function: GI.FunctionInfo | GI.VFuncInfo, in_class: Optional[Any], @@ -400,7 +448,7 @@ def _build_function_info( static = True # Arguments - (names, args, return_args) = _callable_get_arguments( + (names, args, return_args, return_args_names) = _callable_get_arguments( function, current_namespace, needed_namespaces, True ) args_types = [f"{name}: {args[i]}" for (i, name) in enumerate(names)] @@ -414,6 +462,8 @@ def _build_function_info( return_type = f"{return_args[0]}" # Generate string + doc = _build_function_info_doc(fullname, names, return_args, return_args_names) + doc = f' """\n{doc} """\n ...\n' if doc else "" prepend = "" if constructor: args_types.insert(0, "cls") @@ -428,13 +478,14 @@ def _build_function_info( prepend = "@staticmethod\n" if comment: - return f"{prepend}def {name}({', '.join(str(a) for a in args_types)}) -> {return_type}: ... # {comment}\n" + return f"{prepend}def {name}({', '.join(str(a) for a in args_types)}) -> {return_type}: {'...' if not doc else ''} # {comment}\n{doc}\n" else: - return f"{prepend}def {name}({', '.join(str(a) for a in args_types)}) -> {return_type}: ...\n" + return f"{prepend}def {name}({', '.join(str(a) for a in args_types)}) -> {return_type}: {'...' if not doc else ''} \n{doc}\n" def _wrapped_strip_boolean_result( current_namespace: str, + fullname: str, name: str, function: Any, in_class: Optional[Any], @@ -443,7 +494,7 @@ def _wrapped_strip_boolean_result( real_function = function.__wrapped__ fail_ret = inspect.getclosurevars(function).nonlocals.get("fail_ret") - (_, _, return_args) = _callable_get_arguments( + (_, _, return_args, _) = _callable_get_arguments( real_function, current_namespace, needed_namespaces ) return_args = return_args[1:] # Strip first return value @@ -466,6 +517,7 @@ def _wrapped_strip_boolean_result( return _build_function_info( current_namespace, + fullname, name, real_function, in_class, @@ -477,6 +529,7 @@ def _wrapped_strip_boolean_result( def _build_function( current_namespace: str, + fullname: str, name: str, function: Any, in_class: Optional[Any], @@ -488,12 +541,12 @@ def _build_function( if hasattr(function, "__wrapped__"): if "strip_boolean_result" in str(function): return _wrapped_strip_boolean_result( - current_namespace, name, function, in_class, needed_namespaces + current_namespace, fullname, name, function, in_class, needed_namespaces ) if isinstance(function, GI.FunctionInfo) or isinstance(function, GI.VFuncInfo): return _build_function_info( - current_namespace, name, function, in_class, needed_namespaces + current_namespace, fullname, name, function, in_class, needed_namespaces ) signature: Optional[str] = None @@ -611,10 +664,11 @@ def _gi_build_stub( # Functions for name in sorted(functions): + full_name = _generate_full_name(prefix_name, name) + if hasattr(functions[name], "is_deprecated"): deprecated = functions[name].is_deprecated() if deprecated: - full_name = _generate_full_name(prefix_name, name) message = ( # Currently not implemented: # https://gitlab.gnome.org/GNOME/gobject-introspection/-/issues/80 @@ -634,7 +688,12 @@ def _gi_build_stub( continue ret += _build_function( - current_namespace, name, functions[name], in_class, needed_namespaces + current_namespace, + full_name, + name, + functions[name], + in_class, + needed_namespaces, ) if ret and functions: @@ -877,7 +936,7 @@ def _gi_build_stub( o = getattr(obj, key) if isinstance(o, GI.FunctionInfo): function_ret = _build_function( - current_namespace, key, o, obj, needed_namespaces + current_namespace, full_name, key, o, obj, needed_namespaces ) for line in function_ret.splitlines(): ret += " " + line + "\n" @@ -920,7 +979,7 @@ def _gi_build_stub( o = getattr(obj, key) if isinstance(o, GI.FunctionInfo): function_ret = _build_function( - current_namespace, key, o, obj, needed_namespaces + current_namespace, full_name, key, o, obj, needed_namespaces ) for line in function_ret.splitlines(): ret += " " + line + "\n" @@ -983,7 +1042,7 @@ def start(module: str, version: str, overrides: dict[str, str]) -> str: args = parser.parse_args() - DEPRECATION_DOCS = gir.load_gir(args.module, args.version) + DOCS, DEPRECATION_DOCS = gir.load_gir(args.module, args.version) if args.output: overrides: dict[str, str] = {} diff --git a/tools/gir.py b/tools/gir.py index 2502aa5b..98a2d704 100644 --- a/tools/gir.py +++ b/tools/gir.py @@ -7,6 +7,12 @@ import gi from gi.repository import GLib +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", +} + def _get_gir_path(girname: str) -> str: searchdirs: list[str] = [] @@ -35,28 +41,37 @@ def _get_gir_path(girname: str) -> str: 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} +def _docs( + root: ET.Element, + parent_map: dict[ET.Element, ET.Element], + tag: str, + remove_newlines=False, +) -> dict[str, str]: + docs: dict[str, str] = {} - for child in gir_root.iterfind(".//core:doc-deprecated", ns): + for child in root.iterfind(f".//core:{tag}", NS): parents: list[str] = [] - parent = gir_parent_map[child] - while True: + parent = parent_map[child] + while parent: try: parents.insert(0, parent.attrib["name"]) except KeyError: - break - parent = gir_parent_map[parent] - deprecation_docs[".".join(parents[1:])] = ( - cast(str, child.text).replace("\n", "").replace('"', '\\"') - ) + if "return-value" in parent.tag: + parents.insert(0, "$return-value") + pass + parent = parent_map.get(parent, None) + text = cast(str, child.text).replace('"', '\\"') + if remove_newlines: + text = text.replace("\n", "") + docs[".".join(parents[1:])] = text + return docs + + +def load_gir(module: str, version: str) -> tuple[dict[str, str], dict[str, str]]: + 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} - return deprecation_docs + return _docs(gir_root, gir_parent_map, "doc"), _docs( + gir_root, gir_parent_map, "doc-deprecated", True + )