diff --git a/qrenderdoc/Code/pyrenderdoc/parse_reflection.py b/qrenderdoc/Code/pyrenderdoc/parse_reflection.py index 48f3080f5..6ede6c0fd 100644 --- a/qrenderdoc/Code/pyrenderdoc/parse_reflection.py +++ b/qrenderdoc/Code/pyrenderdoc/parse_reflection.py @@ -1,9 +1,10 @@ import ast import inspect import sys +import enum import struct import builtins -from typing import List, Dict, Any, Tuple, Set, Callable, TypeVar, Optional +from typing import List, Dict, Any, Tuple, Set, Union, Callable, TypeVar, Optional # return whether an object is a specialisation of the given generic, @@ -30,6 +31,45 @@ def _is_scope_node(node): ) +def _expr_to_str(node) -> str: + if isinstance(node, ast.Name): + return node.id + if isinstance(node, ast.alias): + return f"{node.name} as {node.asname}" + if isinstance(node, ast.Attribute): + return _expr_to_str(node.value) + "." + node.attr + if isinstance(node, ast.NamedExpr): + return f"({_expr_to_str(node.target)} := {_expr_to_str(node.value)})" + if isinstance(node, ast.Constant): + return str(node.value) + if isinstance(node, ast.Subscript): + return f"{_expr_to_str(node.value)}[{_expr_to_str(node.slice)}]" + if isinstance(node, ast.Slice): + ret = f"{_expr_to_str(node.lower)}:{_expr_to_str(node.upper)}" + if node.step is not None: + ret += _expr_to_str(node.step) + return ret + if isinstance(node, ast.Tuple): + ret = ",".join([_expr_to_str(x) for x in node.elts]) + return f"({ret})" + if isinstance(node, ast.List): + ret = ",".join([_expr_to_str(x) for x in node.elts]) + return f"[{ret}]" + + # legacy types before consolidation into constant + if sys.version_info < (3, 8): + if isinstance(node, ast.Num): + return str(node.value) + if isinstance(node, ast.Str): + return str(node.value) + if isinstance(node, ast.Bytes): + return str(node.value) + if isinstance(node, ast.NameConstant): + return str(node.value) + + return "..." + + def _lookup_attrpath(x: Any, path: str) -> Any: paths = path.split(".") while x is not None and len(paths) > 0: @@ -117,6 +157,27 @@ def _commentlines(text, first_comment_line): return "\n".join(lines) +# remove any common prefix of whitespace in all lines in the string +def _remove_space_prefix(string: str, maxlines: int = 0) -> str: + if string is None: + return "" + lines = [l for l in string.splitlines() if l != ""] + prefix = -1 + for l in lines: + if l.strip() == "": + continue + p = len(l) - len(l.lstrip()) + if prefix == -1 or p < prefix: + prefix = p + if prefix == -1: + prefix = 0 + if maxlines > 0: + if len(lines) > maxlines: + lines = lines[:maxlines] + lines.append((prefix * " ") + "...") + return "\n".join([l[prefix:] for l in lines if l != ""]) + + # replace the contents of all strings with 'x' so that # they don't affect any bracket/brace/etc parsing. # respect escaping @@ -1739,6 +1800,168 @@ class PyReflector: pass return Any + def get_location_tooltip(self, line: int, col: int) -> str: + if self.module is None: + return "" + + try: + expr = self._get_atom_expr(self.module, line, col) + if expr is not None: + loctype = self._get_type(self.scopes[line], expr) + else: + return "" + except: + return "" + + if loctype is Any: + return "" + + if ( + callable(loctype) + and not inspect.isclass(loctype) + and not _is_generic(List, loctype) + and not _is_generic(Tuple, loctype) + and not _is_generic(Dict, loctype) + and not _is_generic(Set, loctype) + and not _is_generic(Optional, loctype) + ): + return self._make_func_tooltip(loctype) + + docappend = "" + ret = "" + if isinstance(expr, ast.Name): + ret = f"{expr.id}: " + elif isinstance(expr, ast.Attribute): + ret = f"{expr.attr}: " + + try: + partype = self._get_type(self.scopes[line], expr.value) + ret = f"{self.get_name(partype)}.{expr.attr}: " + docappend = _remove_space_prefix( + getattr(getattr(partype, expr.attr), "__doc__", ""), 20 + ) + except: + pass + else: + ret = "expression: " + + ret += self.get_name(loctype) + + if docappend != "": + ret += "\n\n" + ret += docappend + + return ret.strip() + + def _make_func_tooltip(self, functype: Any, arg_highlight: int = -1): + ret = "" + + if _is_generic(Callable, functype): + args = functype.__args__ + + retType = args[-1] + if retType == type(None): + retType = None + + ret = "Callable(" + for idx, arg in enumerate(args[0:-1]): + argtext = f"arg{idx+1}" + + argtext += f": {self.get_name(arg)}" + + if idx == arg_highlight: + argtext = f"{argtext}" + + if idx != 0: + ret += ", " + ret += argtext + ret += ")" + if retType is not None: + ret += f" -> {self.get_name(retType)}" + else: + ret += " -> None" + if arg_highlight >= 0: + ret = ret.replace("\n", "
\n") + ret = ret.replace(" ", "  ") + # no docs, we're done here + return ret + + if isinstance(functype, ast.FunctionDef): + ret = functype.name + "(" + for idx, arg in enumerate(functype.args.args): + argtext = arg.arg + + if arg.annotation is not None: + argtext += f": {_expr_to_str(arg.annotation)}" + + if idx == arg_highlight: + argtext = f"{argtext}" + + if idx != 0: + ret += ", " + ret += argtext + if len(functype.args.posonlyargs) > 0: + ret += ", /" + for arg in functype.args.posonlyargs: + ret += ", " + ret = arg.arg + + if arg.type_comment is not None: + ret += f": {arg.type_comment}" + if len(functype.args.kwonlyargs) > 0: + ret += ", *" + for arg in functype.args.kwonlyargs: + ret += ", " + ret = arg.arg + + if arg.type_comment is not None: + ret += f": {arg.type_comment}" + ret += ")" + if functype.returns is not None: + ret += f" -> {self.get_name(self._get_type(self.scopes[functype.lineno], functype.returns))}" + else: + ret += " -> None" + if arg_highlight >= 0: + ret = ret.replace("\n", "
\n") + ret = ret.replace(" ", "  ") + # no docs, we're done here + return ret + + if not callable(functype): + return ret + + try: + sig = inspect.signature(functype) + ret = self.get_name(functype) + "(" + first = True + for idx, arg in enumerate(sig.parameters): + if first and arg == "self": + continue + if not first: + ret += ", " + first = False + argtext = arg + annot = sig.parameters[arg].annotation + if annot is not None and annot != "": + argtext += f": {self.get_name(annot)}" + + if idx == arg_highlight: + argtext = f"{argtext}" + + ret += argtext + ret += ")" + if sig.return_annotation is not inspect.Signature.empty: + ret += f" -> {self.get_name(sig.return_annotation)}" + else: + ret += " -> None" + except: + ret = self.get_name(functype) + "() # unknown signature" + ret += "\n\n" + ret += _remove_space_prefix(getattr(functype, "__doc__", ""), 20) + if arg_highlight >= 0: + ret = ret.replace("\n", "
\n") + return ret.strip() + def get_autocompletion(self, line: int, expr: str) -> Tuple[List[str], int]: expr = _get_trailing_expr(expr).strip() @@ -1834,7 +2057,7 @@ class PyReflector: pass return [], 0 - def get_funccompletion(self, line: int, expr: str) -> Tuple[str, str, int]: + def get_funccompletion(self, line: int, expr: str) -> Tuple[str, str, str]: func, argidx = _get_func_arg(_get_trailing_expr(expr + ")")) try: @@ -1843,7 +2066,7 @@ class PyReflector: node = ast.parse(src) if not isinstance(node, ast.Module) or len(node.body) == []: - return "", "", -1 + return "", "", "" node = node.body[0] curscope = self.scopes[min(len(self.scopes) - 1, line)] @@ -1851,7 +2074,7 @@ class PyReflector: func_type = self._get_type(curscope, node) if func_type == Any: - return "", "", -1 + return "", "", "" if _is_generic(Callable, func_type) and self._last_ident is not None: func_scope = self.scopes[self._last_ident.line] @@ -1861,25 +2084,70 @@ class PyReflector: if isinstance(func_scope.parsed, ast.FunctionDef): func_node = func_scope.parsed if argidx < len(func_node.args.args): - return func, func_node.args.args[argidx].arg, argidx - return func, f"arg{argidx+1}", argidx + return ( + func, + func_node.args.args[argidx].arg, + self._make_func_tooltip(func_node, argidx), + ) + else: + return ( + func, + "", + self._make_func_tooltip(func_node, -1), + ) + return ( + func, + f"arg{argidx+1}", + self._make_func_tooltip(func_type, argidx), + ) sig = inspect.signature(func_type) - if argidx < len(sig.parameters): - # skip invisible self, when looking at methods - if list(sig.parameters)[0] == 'self': - argidx += 1 - return func, list(sig.parameters)[argidx], argidx + params = list(sig.parameters) + # skip invisible self, when looking at methods + if params[0] == "self": + argidx += 1 + if argidx < len(params): + return func, params[argidx], self._make_func_tooltip(func_type, argidx) + return func, "", self._make_func_tooltip(func_type) except: pass - return "", "", -1 + return "", "", "" # try to get a friendly name for a type based on its parent class and module def get_name(self, obj: Any) -> str: + if isinstance(obj, str): + return obj + name = "" + generics = [ + (Tuple, "Tuple"), + (List, "List"), + (Dict, "Dict"), + (Set, "Set"), + ] + for g, n in generics: + if _is_generic(g, obj): + args = ", ".join([self.get_name(a) for a in obj.__args__]) + return f"{n}[{args}]" + + if _is_generic(Callable, obj): + ret_type = self.get_name(obj.__args__[-1]) + args = ", ".join([self.get_name(a) for a in obj.__args__[:-1]]) + return f"Callable[[{args}], {ret_type}]" + + # identify Optional[] looking like Union[x, None] + if _is_generic(Union, obj): + if len(obj.__args__) == 2: + non_none = [ + self.get_name(a) + for a in obj.__args__ + if a is not None and a is not type(None) + ] + return f"Optional[{non_none[0]}]" + if hasattr(obj, "__objclass__"): cl = obj.__objclass__ @@ -1889,29 +2157,69 @@ class PyReflector: membernames = [x for x in dir(cl) if getattr(cl, x) == obj] if len(membernames) != 1: - return "" + name = "" + else: + name += membernames[0] - name += membernames[0] + if hasattr(cl, "__module__") and cl.__module__ != "builtins": + modname = getattr(cl, "__module__") - if hasattr(cl, "__module__") and cl.__module__ != "builtins": - name = f"{cl.__module__}.{name}" - elif hasattr(obj, "__module__"): + for k in PyReflector.alias_modules.keys(): + if PyReflector.alias_modules[k] == sys.modules[modname]: + modname = k + break + + name = f"{modname}.{name}" + + if name == "" and hasattr(obj, "__module__"): mod = obj.__module__ - if mod not in sys.modules: - return "" + if mod in sys.modules: + mod = sys.modules[mod] - mod = sys.modules[mod] + qualname = getattr(obj, "__qualname__", "") - membernames = [x for x in dir(mod) if getattr(mod, x) == obj] + if _lookup_attrpath(mod, qualname) != obj: + membernames = [x for x in dir(mod) if getattr(mod, x) == obj] - if len(membernames) != 1: - return "" + if len(membernames) != 1: + qualname = "" + else: + qualname = membernames[0] - if obj.__module__ == "builtins": - return membernames[0] + if qualname != "": + if obj.__module__ == "builtins": + return qualname.split(".")[-1] - name = f"{obj.__module__}.{membernames[0]}" + modname = obj.__module__ + + for k in PyReflector.alias_modules.keys(): + package = getattr(sys.modules[modname], "__package__", "") + if PyReflector.alias_modules[k] == sys.modules[modname] or ( + package != "" + and PyReflector.alias_modules[k] == sys.modules[package] + ): + modname = k + break + + name = f"{modname}.{qualname}" + + if ( + name == "" + and hasattr(obj, "__qualname__") + and (not hasattr(obj, "__module__") or obj.__module__ != "typing") + ): + return getattr(obj, "__qualname__") + + if name == "" and hasattr(obj, "__name__"): + name = getattr(obj, "__name__").split(".")[-1] + + for k in PyReflector.alias_modules.keys(): + if PyReflector.alias_modules[k] == obj: + return k + + if name == "": + name = str(obj) return name @@ -2810,6 +3118,11 @@ if __name__ == "impossible": # PARAM: foobar # FUNCCOMPLETE TEST + # ENTRY: foo. method(func(), + # CALLTYPE: foo. method + # PARAM: foobar + # FUNCCOMPLETE TEST + # ENTRY: foo.method(func(), randint( # CALLTYPE: randint # PARAM: a