aboutsummaryrefslogtreecommitdiffstats
path: root/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/parser.py
diff options
context:
space:
mode:
Diffstat (limited to 'sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/parser.py')
-rw-r--r--sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/parser.py552
1 files changed, 552 insertions, 0 deletions
diff --git a/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/parser.py b/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/parser.py
new file mode 100644
index 000000000..9b48ab442
--- /dev/null
+++ b/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/parser.py
@@ -0,0 +1,552 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+import ast
+import enum
+import keyword
+import os
+import re
+import sys
+import typing
+import warnings
+
+from types import SimpleNamespace
+from shibokensupport.signature.mapping import (type_map, update_mapping,
+ namespace, _NotCalled, ResultVariable, ArrayLikeVariable) # noqa E:128
+from shibokensupport.signature.lib.tool import build_brace_pattern
+
+_DEBUG = False
+LIST_KEYWORDS = False
+
+"""
+parser.py
+
+This module parses the signature text and creates properties for the
+signature objects.
+
+PySide has a new function 'CppGenerator::writeSignatureInfo()'
+that extracts the gathered information about the function arguments
+and defaults as good as it can. But what PySide generates is still
+very C-ish and has many constants that Python doesn't understand.
+
+The function 'try_to_guess()' below understands a lot of PySide's
+peculiar way to assume local context. If it is able to do the guess,
+then the result is inserted into the dict, so the search happens
+not again. For everything that is not covered by these automatic
+guesses, we provide an entry in 'type_map' that resolves it.
+
+In effect, 'type_map' maps text to real Python objects.
+"""
+
+
+def _get_flag_enum_option():
+ from shiboken6 import (__version_info__ as ver, # noqa F:401
+ __minimum_python_version__ as pyminver,
+ __maximum_python_version__ as pymaxver)
+
+ # PYSIDE-1735: Use the new Enums per default if version is >= 6.4
+ # This decides between delivered vs. dev versions.
+ # When 6.4 is out, the switching mode will be gone.
+ flag = ver[:2] >= (6, 4)
+ envname = "PYSIDE6_OPTION_PYTHON_ENUM"
+ sysname = envname.lower()
+ opt = os.environ.get(envname)
+ if opt:
+ opt = opt.lower()
+ if opt in ("yes", "on", "true"):
+ flag = True
+ elif opt in ("no", "off", "false"):
+ flag = False
+ else:
+ # instead of a simple int() conversion, let's allow for "0xf" or "0b1111"
+ try:
+ flag = ast.literal_eval(opt)
+ except Exception:
+ flag = False # turn a forbidden option into an error
+ elif hasattr(sys, sysname):
+ opt2 = flag = getattr(sys, sysname)
+ if not isinstance(flag, int):
+ flag = False # turn a forbidden option into an error
+ p = f"\n *** Python is at version {'.'.join(map(str, pyminver or (0,)))} now."
+ q = f"\n *** PySide is at version {'.'.join(map(str, ver[:2]))} now."
+ # _PepUnicode_AsString: Fix a broken promise
+ if pyminver and pyminver >= (3, 10):
+ warnings.warn(f"{p} _PepUnicode_AsString can now be replaced by PyUnicode_AsUTF8! ***")
+ # PYSIDE-1960: Emit a warning when we may remove bufferprocs_py37.(cpp|h)
+ if pyminver and pyminver >= (3, 11):
+ warnings.warn(f"{p} The files bufferprocs_py37.(cpp|h) should be removed ASAP! ***")
+ # PYSIDE-1735: Emit a warning when we should maybe evict forgiveness mode
+ if ver[:2] >= (7, 0):
+ warnings.warn(f"{q} Please drop Enum forgiveness mode in `mangled_type_getattro` ***")
+ # PYSIDE-2404: Emit a warning when we should drop uppercase offset constants
+ if ver[:2] >= (7, 0):
+ warnings.warn(f"{q} Please drop uppercase type offsets in `copyOffsetEnumStream` ***")
+ # normalize the sys attribute
+ setattr(sys, sysname, flag)
+ os.environ[envname] = str(flag)
+ if int(flag) == 0:
+ raise RuntimeError(f"Old Enums are no longer supported. int({opt or opt2}) evaluates to 0)")
+ return flag
+
+
+class EnumSelect(enum.Enum):
+ # PYSIDE-1735: Here we could save object.value expressions by using IntEnum.
+ # But it is nice to use just an Enum for selecting Enum version.
+ OLD = 1
+ NEW = 2
+ SELECTION = NEW if _get_flag_enum_option() else OLD
+
+
+def dprint(*args, **kw):
+ if _DEBUG:
+ import pprint
+ for arg in args:
+ pprint.pprint(arg)
+ sys.stdout.flush()
+
+
+_cache = {}
+
+
+def _parse_arglist(argstr):
+ # The following is a split re. The string is broken into pieces which are
+ # between the recognized strings. Because the re has groups, both the
+ # strings and the separators are returned, where the strings are not
+ # interesting at all: They are just the commata.
+ key = "_parse_arglist"
+ if key not in _cache:
+ regex = build_brace_pattern(level=3, separators=",")
+ _cache[key] = re.compile(regex, flags=re.VERBOSE)
+ split = _cache[key].split
+ # Note: this list is interspersed with "," and surrounded by ""
+ return [x.strip() for x in split(argstr) if x.strip() not in ("", ",")]
+
+
+def _parse_line(line):
+ line_re = r"""
+ ((?P<multi> ([0-9]+)) : )? # the optional multi-index
+ (?P<funcname> \w+(\.\w+)*) # the function name
+ \( (?P<arglist> .*?) \) # the argument list
+ ( -> (?P<returntype> .*) )? # the optional return type
+ $
+ """
+ matches = re.match(line_re, line, re.VERBOSE)
+ if not matches:
+ raise SystemError("Error parsing line:", repr(line))
+ ret = SimpleNamespace(**matches.groupdict())
+ # PYSIDE-1095: Handle arbitrary default expressions
+ argstr = ret.arglist.replace("->", ".deref.")
+ arglist = _parse_arglist(argstr)
+ args = []
+ for idx, arg in enumerate(arglist):
+ tokens = arg.split(":")
+ if len(tokens) < 2 and idx == 0 and tokens[0] in ("self", "cls"):
+ tokens = 2 * tokens # "self: self"
+ if len(tokens) != 2:
+ # This should never happen again (but who knows?)
+ raise SystemError(f'Invalid argument "{arg}" in "{line}".')
+ name, ann = tokens
+ if name in keyword.kwlist:
+ if LIST_KEYWORDS:
+ print("KEYWORD", ret)
+ name = name + "_"
+ if "=" in ann:
+ ann, default = ann.split("=", 1)
+ tup = name, ann, default
+ else:
+ tup = name, ann
+ args.append(tup)
+ ret.arglist = args
+ multi = ret.multi
+ if multi is not None:
+ ret.multi = int(multi)
+ funcname = ret.funcname
+ parts = funcname.split(".")
+ if parts[-1] in keyword.kwlist:
+ ret.funcname = funcname + "_"
+ return vars(ret)
+
+
+def _using_snake_case():
+ # Note that this function should stay here where we use snake_case.
+ if "PySide6.QtCore" not in sys.modules:
+ return False
+ from PySide6.QtCore import QDir
+ return hasattr(QDir, "cd_up")
+
+
+def _handle_instance_fixup(thing):
+ """
+ Default expressions using instance methods like
+ (...,device=QPointingDevice.primaryPointingDevice())
+ need extra handling for snake_case. These are:
+ QPointingDevice.primaryPointingDevice()
+ QInputDevice.primaryKeyboard()
+ QKeyCombination.fromCombined(0)
+ QSslConfiguration.defaultConfiguration()
+ """
+ match = re.search(r"\w+\(", thing)
+ if not match:
+ return thing
+ start, stop = match.start(), match.end() - 1
+ pre, func, args = thing[:start], thing[start:stop], thing[stop:]
+ if func[0].isupper() or func.startswith("gl") and func[2:3].isupper():
+ return thing
+ # Now convert this string to snake case.
+ snake_func = ""
+ for idx, char in enumerate(func):
+ if char.isupper():
+ if idx and func[idx - 1].isupper():
+ # two upper chars are forbidden
+ return thing
+ snake_func += f"_{char.lower()}"
+ else:
+ snake_func += char
+ return f"{pre}{snake_func}{args}"
+
+
+def make_good_value(thing, valtype):
+ # PYSIDE-1019: Handle instance calls (which are really seldom)
+ if "(" in thing and _using_snake_case():
+ thing = _handle_instance_fixup(thing)
+ try:
+ if thing.endswith("()"):
+ thing = f'Default("{thing[:-2]}")'
+ else:
+ # PYSIDE-1735: Use explicit globals and locals because of a bug in VsCode
+ ret = eval(thing, globals(), namespace)
+ if valtype and repr(ret).startswith("<"):
+ thing = f'Instance("{thing}")'
+ return eval(thing, globals(), namespace)
+ except Exception:
+ pass
+
+
+def try_to_guess(thing, valtype):
+ if "." not in thing and "(" not in thing:
+ text = f"{valtype}.{thing}"
+ ret = make_good_value(text, valtype)
+ if ret is not None:
+ return ret
+ typewords = valtype.split(".")
+ valwords = thing.split(".")
+ braceless = valwords[0] # Yes, not -1. Relevant is the overlapped word.
+ if "(" in braceless:
+ braceless = braceless[:braceless.index("(")]
+ for idx, w in enumerate(typewords):
+ if w == braceless:
+ text = ".".join(typewords[:idx] + valwords)
+ ret = make_good_value(text, valtype)
+ if ret is not None:
+ return ret
+ return None
+
+
+def get_name(thing):
+ if isinstance(thing, type):
+ return getattr(thing, "__qualname__", thing.__name__)
+ else:
+ return thing.__name__
+
+
+def _resolve_value(thing, valtype, line):
+ if thing in ("0", "None") and valtype:
+ if valtype.startswith("PySide6.") or valtype.startswith("typing."):
+ return None
+ map = type_map[valtype]
+ # typing.Any: '_SpecialForm' object has no attribute '__name__'
+ name = get_name(map) if hasattr(map, "__name__") else str(map)
+ thing = f"zero({name})"
+ if thing in type_map:
+ return type_map[thing]
+ res = make_good_value(thing, valtype)
+ if res is not None:
+ type_map[thing] = res
+ return res
+ res = try_to_guess(thing, valtype) if valtype else None
+ if res is not None:
+ type_map[thing] = res
+ return res
+ warnings.warn(f"""pyside_type_init:_resolve_value
+
+ UNRECOGNIZED: {thing!r}
+ OFFENDING LINE: {line!r}
+ """, RuntimeWarning)
+ return thing
+
+
+def _resolve_arraytype(thing, line):
+ search = re.search(r"\[(\d*)\]$", thing)
+ thing = thing[:search.start()]
+ if thing.endswith("]"):
+ thing = _resolve_arraytype(thing, line)
+ if search.group(1):
+ # concrete array, use a tuple
+ nelem = int(search.group(1))
+ thing = ", ".join([thing] * nelem)
+ thing = "Tuple[" + thing + "]"
+ else:
+ thing = "QList[" + thing + "]"
+ return thing
+
+
+def to_string(thing):
+ # This function returns a string that creates the same object.
+ # It is absolutely crucial that str(eval(thing)) == str(thing),
+ # i.e. it must be an idempotent mapping.
+ if isinstance(thing, str):
+ return thing
+ if hasattr(thing, "__name__") and thing.__module__ != "typing":
+ m = thing.__module__
+ dot = "." in str(thing) or m not in (thing.__qualname__, "builtins")
+ name = get_name(thing)
+ ret = m + "." + name if dot else name
+ assert (eval(ret, globals(), namespace))
+ return ret
+ # Note: This captures things from the typing module:
+ return str(thing)
+
+
+matrix_pattern = "PySide6.QtGui.QGenericMatrix"
+
+
+def handle_matrix(arg):
+ n, m, typstr = tuple(map(lambda x: x.strip(), arg.split(",")))
+ assert typstr == "float"
+ result = f"PySide6.QtGui.QMatrix{n}x{m}"
+ return eval(result, globals(), namespace)
+
+
+def _resolve_type(thing, line, level, var_handler, func_name=None):
+ # manual set of 'str' instead of 'bytes'
+ if func_name:
+ new_thing = (func_name, thing)
+ if new_thing in type_map:
+ return type_map[new_thing]
+
+ # Capture total replacements, first. Happens in
+ # "PySide6.QtCore.QCborStreamReader.StringResult[PySide6.QtCore.QByteArray]"
+ if thing in type_map:
+ return type_map[thing]
+
+ # Now the nested structures are handled.
+ if "[" in thing:
+ # handle primitive arrays
+ if re.search(r"\[\d*\]$", thing):
+ thing = _resolve_arraytype(thing, line)
+ # Handle a container return type. (see PYSIDE-921 in cppgenerator.cpp)
+ contr, thing = re.match(r"(.*?)\[(.*?)\]$", thing).groups()
+ # Special case: Handle the generic matrices.
+ if contr == matrix_pattern:
+ return handle_matrix(thing)
+ contr = var_handler(_resolve_type(contr, line, level + 1, var_handler))
+ if isinstance(contr, _NotCalled):
+ raise SystemError("Container types must exist:", repr(contr))
+ contr = to_string(contr)
+ pieces = []
+ for part in _parse_arglist(thing):
+ part = var_handler(_resolve_type(part, line, level + 1, var_handler))
+ if isinstance(part, _NotCalled):
+ # fix the tag (i.e. "Missing") by repr
+ part = repr(part)
+ pieces.append(to_string(part))
+ thing = ", ".join(pieces)
+ result = f"{contr}[{thing}]"
+ # PYSIDE-1538: Make sure that the eval does not crash.
+ try:
+ return eval(result, globals(), namespace)
+ except Exception:
+ warnings.warn(f"""pyside_type_init:_resolve_type
+
+ UNRECOGNIZED: {result!r}
+ OFFENDING LINE: {line!r}
+ """, RuntimeWarning)
+ return _resolve_value(thing, None, line)
+
+
+def _handle_generic(obj, repl):
+ """
+ Assign repl if obj is an ArrayLikeVariable
+
+ This is a neat trick. Example:
+
+ obj repl result
+ ---------------------- -------- ---------
+ ArrayLikeVariable List List
+ ArrayLikeVariable(str) List List[str]
+ ArrayLikeVariable Sequence Sequence
+ ArrayLikeVariable(str) Sequence Sequence[str]
+ """
+ if isinstance(obj, ArrayLikeVariable):
+ return repl[obj.type]
+ if isinstance(obj, type) and issubclass(obj, ArrayLikeVariable):
+ # was "if obj is ArrayLikeVariable"
+ return repl
+ return obj
+
+
+def handle_argvar(obj):
+ """
+ Decide how array-like variables are resolved in arguments
+
+ Currently, the best approximation is types.Sequence.
+ We want to change that to types.Iterable in the near future.
+ """
+ return _handle_generic(obj, typing.Sequence)
+
+
+def handle_retvar(obj):
+ """
+ Decide how array-like variables are resolved in results
+
+ This will probably stay typing.List forever.
+ """
+ return _handle_generic(obj, typing.List)
+
+
+def calculate_props(line):
+ parsed = SimpleNamespace(**_parse_line(line.strip()))
+ arglist = parsed.arglist
+ annotations = {}
+ _defaults = []
+ for idx, tup in enumerate(arglist):
+ name, ann = tup[:2]
+ if ann == "...":
+ name = "*args" if name.startswith("arg_") else "*" + name
+ # copy the patched fields back
+ ann = 'nullptr' # maps to None
+ tup = name, ann
+ arglist[idx] = tup
+ annotations[name] = _resolve_type(ann, line, 0, handle_argvar, parsed.funcname)
+ if len(tup) == 3:
+ default = _resolve_value(tup[2], ann, line)
+ _defaults.append(default)
+ defaults = tuple(_defaults)
+ returntype = parsed.returntype
+ if isinstance(returntype, str) and returntype.startswith("("):
+ # PYSIDE-1588: Simplify the handling of returned tuples for now.
+ # Later we might create named tuples, instead.
+ returntype = "Tuple"
+ # PYSIDE-1383: We need to handle `None` explicitly.
+ annotations["return"] = (_resolve_type(returntype, line, 0, handle_retvar)
+ if returntype is not None else None)
+ props = SimpleNamespace()
+ props.defaults = defaults
+ props.kwdefaults = {}
+ props.annotations = annotations
+ props.varnames = tuple(tup[0] for tup in arglist)
+ funcname = parsed.funcname
+ shortname = funcname[funcname.rindex(".") + 1:]
+ props.name = shortname
+ props.multi = parsed.multi
+ fix_variables(props, line)
+ return vars(props)
+
+
+def fix_variables(props, line):
+ annos = props.annotations
+ if not any(isinstance(ann, (ResultVariable, ArrayLikeVariable))
+ for ann in annos.values()):
+ return
+ retvar = annos.get("return", None)
+ if retvar and isinstance(retvar, (ResultVariable, ArrayLikeVariable)):
+ # Special case: a ResultVariable which is the result will always be an array!
+ annos["return"] = retvar = typing.List[retvar.type]
+ varnames = list(props.varnames)
+ defaults = list(props.defaults)
+ diff = len(varnames) - len(defaults)
+
+ safe_annos = annos.copy()
+ retvars = [retvar] if retvar else []
+ deletions = []
+ for idx, name in enumerate(varnames):
+ ann = safe_annos[name]
+ if isinstance(ann, ArrayLikeVariable):
+ ann = typing.Sequence[ann.type]
+ annos[name] = ann
+ if not isinstance(ann, ResultVariable):
+ continue
+ # We move the variable to the end and remove it.
+ # PYSIDE-1409: If the variable was the first arg, we move it to the front.
+ # XXX This algorithm should probably be replaced by more introspection.
+ retvars.insert(0 if idx == 0 else len(retvars), ann.type)
+ deletions.append(idx)
+ del annos[name]
+ for idx in reversed(deletions):
+ # varnames: 0 1 2 3 4 5 6 7
+ # defaults: 0 1 2 3 4
+ # diff: 3
+ del varnames[idx]
+ if idx >= diff:
+ del defaults[idx - diff]
+ else:
+ diff -= 1
+ if retvars:
+ retvars = list(handle_retvar(rv) if isinstance(rv, ArrayLikeVariable) else rv
+ for rv in retvars)
+ if len(retvars) == 1:
+ returntype = retvars[0]
+ else:
+ retvars_str = ", ".join(map(to_string, retvars))
+ typestr = f"typing.Tuple[{retvars_str}]"
+ returntype = eval(typestr, globals(), namespace)
+ props.annotations["return"] = returntype
+ props.varnames = tuple(varnames)
+ props.defaults = tuple(defaults)
+
+
+def fixup_multilines(lines):
+ """
+ Multilines can collapse when certain distinctions between C++ types
+ vanish after mapping to Python.
+ This function fixes this by re-computing multiline-ness.
+ """
+ res = []
+ multi_lines = []
+ for line in lines:
+ multi = re.match(r"([0-9]+):", line)
+ if multi:
+ idx, rest = int(multi.group(1)), line[multi.end():]
+ multi_lines.append(rest)
+ if idx > 0:
+ continue
+ # remove duplicates
+ multi_lines = sorted(set(multi_lines))
+ # renumber or return a single line
+ nmulti = len(multi_lines)
+ if nmulti > 1:
+ for idx, line in enumerate(multi_lines):
+ res.append(f"{nmulti-idx-1}:{line}")
+ else:
+ res.append(multi_lines[0])
+ multi_lines = []
+ else:
+ res.append(line)
+ return res
+
+
+def pyside_type_init(type_key, sig_strings):
+ dprint()
+ dprint(f"Initialization of type key '{type_key}'")
+ update_mapping()
+ lines = fixup_multilines(sig_strings)
+ ret = {}
+ multi_props = []
+ for line in lines:
+ props = calculate_props(line)
+ shortname = props["name"]
+ multi = props["multi"]
+ if multi is None:
+ ret[shortname] = props
+ dprint(props)
+ else:
+ multi_props.append(props)
+ if multi > 0:
+ continue
+ multi_props = {"multi": multi_props}
+ ret[shortname] = multi_props
+ dprint(multi_props)
+ multi_props = []
+ return ret
+
+# end of file