diff options
Diffstat (limited to 'sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/parser.py')
-rw-r--r-- | sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/parser.py | 250 |
1 files changed, 170 insertions, 80 deletions
diff --git a/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/parser.py b/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/parser.py index d737c2a54..9b48ab442 100644 --- a/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/parser.py +++ b/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/parser.py @@ -1,52 +1,19 @@ -############################################################################# -## -## Copyright (C) 2019 The Qt Company Ltd. -## Contact: https://www.qt.io/licensing/ -## -## This file is part of Qt for Python. -## -## $QT_BEGIN_LICENSE:LGPL$ -## Commercial License Usage -## Licensees holding valid commercial Qt licenses may use this file in -## accordance with the commercial license agreement provided with the -## Software or, alternatively, in accordance with the terms contained in -## a written agreement between you and The Qt Company. For licensing terms -## and conditions see https://www.qt.io/terms-conditions. For further -## information use the contact form at https://www.qt.io/contact-us. -## -## GNU Lesser General Public License Usage -## Alternatively, this file may be used under the terms of the GNU Lesser -## General Public License version 3 as published by the Free Software -## Foundation and appearing in the file LICENSE.LGPL3 included in the -## packaging of this file. Please review the following information to -## ensure the GNU Lesser General Public License version 3 requirements -## will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -## -## GNU General Public License Usage -## Alternatively, this file may be used under the terms of the GNU -## General Public License version 2.0 or (at your option) the GNU General -## Public license version 3 or any later version approved by the KDE Free -## Qt Foundation. The licenses are as published by the Free Software -## Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -## included in the packaging of this file. Please review the following -## information to ensure the GNU General Public License requirements will -## be met: https://www.gnu.org/licenses/gpl-2.0.html and -## https://www.gnu.org/licenses/gpl-3.0.html. -## -## $QT_END_LICENSE$ -## -############################################################################# +# 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 sys +import ast +import enum +import keyword +import os import re +import sys +import typing import warnings -import types -import keyword -import functools + +from types import SimpleNamespace from shibokensupport.signature.mapping import (type_map, update_mapping, - namespace, typing, _NotCalled, ResultVariable, ArrayLikeVariable) -from shibokensupport.signature.lib.tool import (SimpleNamespace, - build_brace_pattern) + namespace, _NotCalled, ResultVariable, ArrayLikeVariable) # noqa E:128 +from shibokensupport.signature.lib.tool import build_brace_pattern _DEBUG = False LIST_KEYWORDS = False @@ -71,6 +38,65 @@ 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 @@ -81,6 +107,7 @@ def dprint(*args, **kw): _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 @@ -103,19 +130,21 @@ def _parse_line(line): ( -> (?P<returntype> .*) )? # the optional return type $ """ - ret = SimpleNamespace(**re.match(line_re, line, re.VERBOSE).groupdict()) + 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: - if idx == 0 and tokens[0] == "self": - tokens = 2 * tokens # "self: self" - else: - # This should never happen again (but who knows?) - raise SystemError(f'Invalid argument "{arg}" in "{line}".') + 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: @@ -138,15 +167,57 @@ def _parse_line(line): 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: - ret = eval(thing, namespace) + # 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, namespace) + return eval(thing, globals(), namespace) except Exception: pass @@ -170,12 +241,14 @@ def try_to_guess(thing, valtype): 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."): @@ -194,7 +267,7 @@ def _resolve_value(thing, valtype, line): if res is not None: type_map[thing] = res return res - warnings.warn(f"""pyside_type_init: + warnings.warn(f"""pyside_type_init:_resolve_value UNRECOGNIZED: {thing!r} OFFENDING LINE: {line!r} @@ -218,40 +291,44 @@ def _resolve_arraytype(thing, line): 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__"): - dot = "." in str(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) - return thing.__module__ + "." + name if dot else name + 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(","))) + 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, namespace) - + return eval(result, globals(), namespace) -debugging_aid = """ -from inspect import currentframe - -def lno(level): - lineno = currentframe().f_back.f_lineno - spaces = level * " " - return f"{lineno}{spaces}" -""" +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] -def _resolve_type(thing, line, level, var_handler): # 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 @@ -262,20 +339,28 @@ def _resolve_type(thing, line, level, var_handler): # 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)) + 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)) + 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}]" - return eval(result, namespace) + # 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) @@ -328,16 +413,20 @@ def calculate_props(line): name, ann = tup[:2] if ann == "...": name = "*args" if name.startswith("arg_") else "*" + name - # copy the pathed fields back + # 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) + 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) @@ -345,9 +434,9 @@ def calculate_props(line): props.defaults = defaults props.kwdefaults = {} props.annotations = annotations - props.varnames = varnames = tuple(tup[0] for tup in arglist) + props.varnames = tuple(tup[0] for tup in arglist) funcname = parsed.funcname - shortname = funcname[funcname.rindex(".")+1:] + shortname = funcname[funcname.rindex(".") + 1:] props.name = shortname props.multi = parsed.multi fix_variables(props, line) @@ -378,7 +467,9 @@ def fix_variables(props, line): if not isinstance(ann, ResultVariable): continue # We move the variable to the end and remove it. - retvars.append(ann.type) + # 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): @@ -391,7 +482,6 @@ def fix_variables(props, line): else: diff -= 1 if retvars: - rvs = [] retvars = list(handle_retvar(rv) if isinstance(rv, ArrayLikeVariable) else rv for rv in retvars) if len(retvars) == 1: @@ -399,7 +489,7 @@ def fix_variables(props, line): else: retvars_str = ", ".join(map(to_string, retvars)) typestr = f"typing.Tuple[{retvars_str}]" - returntype = eval(typestr, namespace) + returntype = eval(typestr, globals(), namespace) props.annotations["return"] = returntype props.varnames = tuple(varnames) props.defaults = tuple(defaults) |