aboutsummaryrefslogtreecommitdiffstats
path: root/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/lib
diff options
context:
space:
mode:
Diffstat (limited to 'sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/lib')
-rw-r--r--sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/lib/__init__.py5
-rw-r--r--sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/lib/enum_sig.py305
-rw-r--r--sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/lib/pyi_generator.py335
-rw-r--r--sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/lib/tool.py111
4 files changed, 756 insertions, 0 deletions
diff --git a/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/lib/__init__.py b/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/lib/__init__.py
new file mode 100644
index 000000000..db4f381f5
--- /dev/null
+++ b/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/lib/__init__.py
@@ -0,0 +1,5 @@
+# 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
+from __future__ import annotations
+
+# this file intentionally left blank
diff --git a/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/lib/enum_sig.py b/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/lib/enum_sig.py
new file mode 100644
index 000000000..9d98d7b1b
--- /dev/null
+++ b/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/lib/enum_sig.py
@@ -0,0 +1,305 @@
+# 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
+from __future__ import annotations
+
+"""
+enum_sig.py
+
+Enumerate all signatures of a class.
+
+This module separates the enumeration process from the formatting.
+It is not easy to adhere to this protocol, but in the end, it paid off
+by producing a lot of clarity.
+"""
+
+import inspect
+import sys
+import types
+from shibokensupport.signature import get_signature as get_sig
+
+
+"""
+PYSIDE-1599: Making sure that pyi files always are tested.
+
+A new problem popped up when supporting true properties:
+When there exists an item named "property", then we cannot use
+the builtin `property` as decorator, but need to prefix it with "builtins".
+
+We scan for such a name in a class, and if there should a property be
+declared in the same class, we use `builtins.property` in the class and
+all sub-classes. The same consideration holds for "overload".
+"""
+
+_normal_functions = (types.BuiltinFunctionType, types.FunctionType)
+if hasattr(sys, "pypy_version_info"):
+ # In PyPy, there are more types because our builtin functions
+ # are created differently in C++ and not in PyPy.
+ _normal_functions += (type(get_sig),)
+
+
+def signal_check(thing):
+ return thing and type(thing) in (Signal, SignalInstance)
+
+
+class ExactEnumerator(object):
+ """
+ ExactEnumerator enumerates all signatures in a module as they are.
+
+ This class is used for generating complete listings of all signatures.
+ An appropriate formatter should be supplied, if printable output
+ is desired.
+ """
+
+ def __init__(self, formatter, result_type=dict):
+ global EnumMeta, Signal, SignalInstance
+ try:
+ # Lazy import
+ from PySide6.QtCore import Qt, Signal, SignalInstance
+ EnumMeta = type(Qt.Key)
+ except ImportError:
+ EnumMeta = Signal = SignalInstance = None
+
+ self.fmt = formatter
+ self.result_type = result_type
+ self.fmt.level = 0
+ self.fmt.is_method = self.is_method
+ self.collision_candidates = {"property", "overload"}
+
+ def is_method(self):
+ """
+ Is this function a method?
+ We check if it is a simple function.
+ """
+ tp = type(self.func)
+ return tp not in _normal_functions
+
+ def section(self):
+ if hasattr(self.fmt, "section"):
+ self.fmt.section()
+
+ def module(self, mod_name):
+ __import__(mod_name)
+ self.fmt.mod_name = mod_name
+ with self.fmt.module(mod_name):
+ module = sys.modules[mod_name]
+ members = inspect.getmembers(module, inspect.isclass)
+ functions = inspect.getmembers(module, inspect.isroutine)
+ ret = self.result_type()
+ self.fmt.class_name = None
+ for class_name, klass in members:
+ self.collision_track = set()
+ ret.update(self.klass(class_name, klass))
+ if len(members):
+ self.section()
+ for func_name, func in functions:
+ ret.update(self.function(func_name, func))
+ if len(functions):
+ self.section()
+ return ret
+
+ def klass(self, class_name, klass):
+ ret = self.result_type()
+ if ("._") in class_name:
+ # This happens when introspecting enum.Enum etc. Python 3.8.8 does not
+ # like this, but we want to remove that, anyway.
+ return ret
+ if "<" in class_name:
+ # This is happening in QtQuick for some reason:
+ # class std::shared_ptr<QQuickItemGrabResult >:
+ # We simply skip over this class.
+ return ret
+ bases_list = []
+ for base in klass.__bases__:
+ name = base.__qualname__
+ if name not in ("object", "property", "type"):
+ name = base.__module__ + "." + name
+ bases_list.append(name)
+ bases_str = ', '.join(bases_list)
+ class_str = f"{class_name}({bases_str})"
+ # class_members = inspect.getmembers(klass)
+ # gives us also the inherited things.
+ class_members = sorted(list(klass.__dict__.items()))
+ subclasses = []
+ functions = []
+ enums = []
+ properties = []
+ signals = []
+ attributes = {}
+
+ for thing_name, thing in class_members:
+ if signal_check(thing):
+ signals.append((thing_name, thing))
+ elif inspect.isclass(thing):
+ # If this is the only member of the class, it causes the stub
+ # to be printed empty without ..., as self.fmt.have_body will
+ # then be True. (Example: QtCore.QCborTag). Skip it to avoid
+ # this problem.
+ if thing_name == "_member_type_":
+ continue
+ subclass_name = ".".join((class_name, thing_name))
+ subclasses.append((subclass_name, thing))
+ elif inspect.isroutine(thing):
+ func_name = thing_name.split(".")[0] # remove ".overload"
+ functions.append((func_name, thing))
+ elif type(type(thing)) is EnumMeta:
+ # take the real enum name, not what is in the dict
+ if not thing_name.startswith("_"):
+ enums.append((thing_name, type(thing).__qualname__, thing))
+ elif isinstance(thing, property):
+ properties.append((thing_name, thing))
+ # Support attributes that have PySide types as values,
+ # but we skip the 'staticMetaObject' that needs
+ # to be defined at a QObject level.
+ elif "PySide" in str(type(thing)) and "QMetaObject" not in str(type(thing)):
+ if class_name not in attributes:
+ attributes[class_name] = {}
+ attributes[class_name][thing_name] = thing
+
+ if thing_name in self.collision_candidates:
+ self.collision_track.add(thing_name)
+
+ init_signature = getattr(klass, "__signature__", None)
+ # sort by class then enum value
+ enums.sort(key=lambda tup: (tup[1], tup[2].value))
+
+ # We want to handle functions and properties together.
+ func_prop = sorted(functions + properties, key=lambda tup: tup[0])
+
+ # find out how many functions create a signature
+ sigs = list(_ for _ in functions if get_sig(_[1]))
+ self.fmt.have_body = bool(subclasses or sigs or properties or enums or # noqa W:504
+ init_signature or signals or attributes)
+
+ with self.fmt.klass(class_name, class_str):
+ self.fmt.level += 1
+ self.fmt.class_name = class_name
+ if hasattr(self.fmt, "enum"):
+ # this is an optional feature
+ if len(enums):
+ self.section()
+ for enum_name, enum_class_name, value in enums:
+ with self.fmt.enum(enum_class_name, enum_name, value.value):
+ pass
+ if hasattr(self.fmt, "signal"):
+ # this is an optional feature
+ if len(signals):
+ self.section()
+ for signal_name, signal in signals:
+ sig_class = type(signal)
+ sig_class_name = f"{sig_class.__qualname__}"
+ sig_str = str(signal)
+ with self.fmt.signal(sig_class_name, signal_name, sig_str):
+ pass
+ if hasattr(self.fmt, "attribute"):
+ if len(attributes):
+ self.section()
+ for class_name, attrs in attributes.items():
+ for attr_name, attr_value in attrs.items():
+ with self.fmt.attribute(attr_name, attr_value):
+ pass
+ if len(subclasses):
+ self.section()
+ for subclass_name, subclass in subclasses:
+ save = self.collision_track.copy()
+ ret.update(self.klass(subclass_name, subclass))
+ self.collision_track = save
+ self.fmt.class_name = class_name
+ if len(subclasses):
+ self.section()
+ ret.update(self.function("__init__", klass))
+ for func_name, func in func_prop:
+ if func_name != "__init__":
+ if isinstance(func, property):
+ ret.update(self.fproperty(func_name, func))
+ else:
+ ret.update(self.function(func_name, func))
+ self.fmt.level -= 1
+ if len(func_prop):
+ self.section()
+ return ret
+
+ @staticmethod
+ def get_signature(func):
+ return get_sig(func)
+
+ def function(self, func_name, func, decorator=None):
+ self.func = func # for is_method()
+ ret = self.result_type()
+ if decorator in self.collision_track:
+ decorator = f"builtins.{decorator}"
+ signature = self.get_signature(func, decorator)
+ if signature is not None:
+ with self.fmt.function(func_name, signature, decorator) as key:
+ ret[key] = signature
+ del self.func
+ return ret
+
+ def fproperty(self, prop_name, prop):
+ ret = self.function(prop_name, prop.fget, type(prop).__qualname__)
+ if prop.fset:
+ ret.update(self.function(prop_name, prop.fset, f"{prop_name}.setter"))
+ if prop.fdel:
+ ret.update(self.function(prop_name, prop.fdel, f"{prop_name}.deleter"))
+ return ret
+
+
+def stringify(signature):
+ if isinstance(signature, list):
+ # remove duplicates which still sometimes occour:
+ ret = set(stringify(sig) for sig in signature)
+ return sorted(ret) if len(ret) > 1 else list(ret)[0]
+ return tuple(str(pv) for pv in signature.parameters.values())
+
+
+class SimplifyingEnumerator(ExactEnumerator):
+ """
+ SimplifyingEnumerator enumerates all signatures in a module filtered.
+
+ There are no default values, no variable
+ names and no self parameter. Only types are present after simplification.
+ The functions 'next' resp. '__next__' are removed
+ to make the output identical for Python 2 and 3.
+ An appropriate formatter should be supplied, if printable output
+ is desired.
+ """
+
+ def function(self, func_name, func):
+ ret = self.result_type()
+ signature = get_sig(func, 'existence')
+ sig = stringify(signature) if signature is not None else None
+ if sig is not None and func_name not in ("next", "__next__", "__div__"):
+ with self.fmt.function(func_name, sig) as key:
+ ret[key] = sig
+ return ret
+
+
+class HintingEnumerator(ExactEnumerator):
+ """
+ HintingEnumerator enumerates all signatures in a module slightly changed.
+
+ This class is used for generating complete listings of all signatures for
+ hinting stubs. Only default values are replaced by "...".
+ """
+
+ def __init__(self, *args, **kwds):
+ super().__init__(*args, **kwds)
+ # We need to provide default signatures for class properties.
+ cls_param = inspect.Parameter("cls", inspect._POSITIONAL_OR_KEYWORD)
+ set_param = inspect.Parameter("arg_1", inspect._POSITIONAL_OR_KEYWORD, annotation=object)
+ self.getter_sig = inspect.Signature([cls_param], return_annotation=object)
+ self.setter_sig = inspect.Signature([cls_param, set_param])
+ self.deleter_sig = inspect.Signature([cls_param])
+
+ def get_signature(self, func, decorator=None):
+ # Class properties don't have signature support (yet).
+ # In that case, produce a fake one.
+ sig = get_sig(func, "hintingstub")
+ if not sig:
+ if decorator:
+ if decorator.endswith(".setter"):
+ sig = self.setter_sig
+ elif decorator.endswith(".deleter"):
+ sig = self.deleter_sig
+ else:
+ sig = self.getter_sig
+ return sig
diff --git a/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/lib/pyi_generator.py b/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/lib/pyi_generator.py
new file mode 100644
index 000000000..641b6693a
--- /dev/null
+++ b/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/lib/pyi_generator.py
@@ -0,0 +1,335 @@
+LICENSE_TEXT = """
+# 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
+from __future__ import annotations
+"""
+
+# flake8: noqa E:402
+
+"""
+pyi_generator.py
+
+This script generates .pyi files for arbitrary modules.
+"""
+
+import argparse
+import io
+import logging
+import os
+import re
+import sys
+import typing
+
+from pathlib import Path
+from contextlib import contextmanager
+from textwrap import dedent
+
+from shibokensupport.signature.lib.enum_sig import HintingEnumerator
+from shibokensupport.signature.lib.tool import build_brace_pattern
+
+# Can we use forward references?
+USE_PEP563 = sys.version_info[:2] >= (3, 7)
+
+indent = " " * 4
+
+
+class Writer(object):
+ def __init__(self, outfile, *args):
+ self.outfile = outfile
+ self.history = [True, True]
+
+ def print(self, *args, **kw):
+ # controlling too much blank lines
+ if self.outfile:
+ if args == () or args == ("",):
+ # We use that to skip too many blank lines:
+ if self.history[-2:] == [True, True]:
+ return
+ print("", file=self.outfile, **kw)
+ self.history.append(True)
+ else:
+ print(*args, file=self.outfile, **kw)
+ self.history.append(False)
+
+
+class Formatter(Writer):
+ """
+ Formatter is formatting the signature listing of an enumerator.
+
+ It is written as context managers in order to avoid many callbacks.
+ The separation in formatter and enumerator is done to keep the
+ unrelated tasks of enumeration and formatting apart.
+ """
+ def __init__(self, outfile, options, *args):
+ # XXX Find out which of these patches is still necessary!
+ self.options = options
+ Writer.__init__(self, outfile, *args)
+ # patching __repr__ to disable the __repr__ of typing.TypeVar:
+ """
+ def __repr__(self):
+ if self.__covariant__:
+ prefix = '+'
+ elif self.__contravariant__:
+ prefix = '-'
+ else:
+ prefix = '~'
+ return prefix + self.__name__
+ """
+ def _typevar__repr__(self):
+ return f"typing.{self.__name__}"
+ # This is no longer necessary for modern typing versions.
+ # Ignore therefore if the repr is read-only and cannot be changed.
+ try:
+ typing.TypeVar.__repr__ = _typevar__repr__
+ except TypeError:
+ pass
+ # Adding a pattern to substitute "Union[T, NoneType]" by "Optional[T]"
+ # I tried hard to replace typing.Optional by a simple override, but
+ # this became _way_ too much.
+ # See also the comment in layout.py .
+ brace_pat = build_brace_pattern(3, ",")
+ pattern = fr"\b Union \s* \[ \s* {brace_pat} \s*, \s* NoneType \s* \]"
+ replace = r"Optional[\1]"
+ optional_searcher = re.compile(pattern, flags=re.VERBOSE)
+
+ def optional_replacer(source):
+ return optional_searcher.sub(replace, str(source))
+ self.optional_replacer = optional_replacer
+ # self.level is maintained by enum_sig.py
+ # self.is_method() is true for non-plain functions.
+
+ def section(self):
+ if self.level == 0:
+ self.print()
+ self.print()
+
+ @contextmanager
+ def module(self, mod_name):
+ self.mod_name = mod_name
+ txt = f"""\
+ # Module `{mod_name}`
+
+ <<IMPORTS>>
+ """
+ self.print(dedent(txt))
+ yield
+
+ @contextmanager
+ def klass(self, class_name, class_str):
+ spaces = indent * self.level
+ while "." in class_name:
+ class_name = class_name.split(".", 1)[-1]
+ class_str = class_str.split(".", 1)[-1]
+ if self.have_body:
+ self.print(f"{spaces}class {class_str}:")
+ else:
+ self.print(f"{spaces}class {class_str}: ...")
+ yield
+
+ @contextmanager
+ def function(self, func_name, signature, decorator=None):
+ if func_name == "__init__":
+ self.print()
+ key = func_name
+ spaces = indent * self.level
+ if isinstance(signature, list):
+ for sig in signature:
+ self.print(f'{spaces}@overload')
+ self._function(func_name, sig, spaces)
+ else:
+ self._function(func_name, signature, spaces, decorator)
+ if func_name == "__init__":
+ self.print()
+ yield key
+
+ def _function(self, func_name, signature, spaces, decorator=None):
+ if decorator:
+ # In case of a PyClassProperty the classmethod decorator is not used.
+ self.print(f'{spaces}@{decorator}')
+ elif self.is_method() and "self" not in signature.parameters:
+ kind = "class" if "cls" in signature.parameters else "static"
+ self.print(f'{spaces}@{kind}method')
+ signature = self.optional_replacer(signature)
+ self.print(f'{spaces}def {func_name}{signature}: ...')
+
+ @contextmanager
+ def enum(self, class_name, enum_name, value):
+ spaces = indent * self.level
+ hexval = hex(value)
+ self.print(f"{spaces}{enum_name:25}: {class_name} = ... # {hexval}")
+ yield
+
+ @contextmanager
+ def attribute(self, attr_name, attr_value):
+ spaces = indent * self.level
+ self.print(f"{spaces}{attr_name:25} = ... # type: {type(attr_value).__qualname__}")
+ yield
+
+ @contextmanager
+ def signal(self, class_name, sig_name, sig_str):
+ spaces = indent * self.level
+ self.print(f"{spaces}{sig_name:25}: ClassVar[{class_name}] = ... # {sig_str}")
+ yield
+
+
+def find_imports(text):
+ return [imp for imp in PySide6.__all__ if f"PySide6.{imp}." in text]
+
+
+FROM_IMPORTS = [
+ (None, ["builtins"]),
+ (None, ["os"]),
+ (None, ["enum"]),
+ ("collections.abc", ["Iterable"]),
+ ("typing", sorted(typing.__all__)),
+ ("PySide6.QtCore", ["PyClassProperty", "Signal", "SignalInstance"]),
+ ("shiboken6", ["Shiboken"]),
+ ]
+
+
+def filter_from_imports(from_struct, text):
+ """
+ Build a reduced new `from` structure (nfs) with found entries, only
+ """
+ nfs = []
+ for mod, imports in from_struct:
+ lis = []
+ nfs.append((mod, lis))
+ for each in imports:
+ # PYSIDE-1603: We search text that is a usage of the class `each`,
+ # but only if the class is not also defined here.
+ if (f"class {each}(") not in text:
+ if re.search(rf"(\b|@){each}\b([^\s\(:]|\n)", text):
+ lis.append(each)
+ # Search if a type is present in the return statement
+ # of function declarations: '... -> here:'
+ if re.search(rf"->.*{each}.*:", text):
+ lis.append(each)
+ if not lis:
+ nfs.pop()
+ return nfs
+
+
+def find_module(import_name, outpath, from_pyside):
+ """
+ Find a module either directly by import, or use the full path,
+ add the path to sys.path and import then.
+ """
+ if from_pyside:
+ # internal mode for generate_pyi.py
+ plainname = import_name.split(".")[-1]
+ outfilepath = Path(outpath) / f"{plainname}.pyi"
+ return import_name, plainname, outfilepath
+ # we are alone in external module mode
+ p = Path(import_name).resolve()
+ if not p.exists():
+ raise ValueError(f"File {p} does not exist.")
+ if not outpath:
+ outpath = p.parent
+ # temporarily add the path and do the import
+ sys.path.insert(0, os.fspath(p.parent))
+ plainname = p.name.split(".")[0]
+ __import__(plainname)
+ sys.path.pop(0)
+ return plainname, plainname, Path(outpath) / (plainname + ".pyi")
+
+
+def generate_pyi(import_name, outpath, options):
+ """
+ Generates a .pyi file.
+ """
+ import_name, plainname, outfilepath = find_module(import_name, outpath, options._pyside_call)
+ top = __import__(import_name)
+ obj = getattr(top, plainname) if import_name != plainname else top
+ if not getattr(obj, "__file__", None) or Path(obj.__file__).is_dir():
+ raise ModuleNotFoundError(f"We do not accept a namespace as module `{plainname}`")
+
+ outfile = io.StringIO()
+ fmt = Formatter(outfile, options)
+ fmt.print(LICENSE_TEXT.strip())
+ if USE_PEP563:
+ fmt.print("from __future__ import annotations")
+ fmt.print()
+ fmt.print(dedent(f'''\
+ """
+ This file contains the exact signatures for all functions in module
+ {import_name}, except for defaults which are replaced by "...".
+ """
+ '''))
+ HintingEnumerator(fmt).module(import_name)
+ fmt.print("# eof")
+ # Postprocess: resolve the imports
+ if options._pyside_call:
+ global PySide6
+ import PySide6
+ with outfilepath.open("w") as realfile:
+ wr = Writer(realfile)
+ outfile.seek(0)
+ while True:
+ line = outfile.readline()
+ if not line:
+ break
+ line = line.rstrip()
+ # we remove the "<<IMPORTS>>" marker and insert imports if needed
+ if line == "<<IMPORTS>>":
+ text = outfile.getvalue()
+ wr.print("import " + import_name)
+ for mod_name in find_imports(text):
+ imp = "PySide6." + mod_name
+ if imp != import_name:
+ wr.print("import " + imp)
+ wr.print()
+ for mod, imports in filter_from_imports(FROM_IMPORTS, text):
+ import_args = ', '.join(imports)
+ if mod is None:
+ # special case, a normal import
+ wr.print(f"import {import_args}")
+ else:
+ wr.print(f"from {mod} import {import_args}")
+ wr.print()
+ wr.print()
+ wr.print("NoneType: TypeAlias = type[None]")
+ wr.print()
+ else:
+ wr.print(line)
+ if not options.quiet:
+ options.logger.info(f"Generated: {outfilepath}")
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ description=dedent("""\
+ pyi_generator.py
+ ----------------
+
+ This script generates the .pyi file for an arbitrary module.
+ You pass in the full path of a compiled, importable module.
+ pyi_generator will try to generate an interface "<module>.pyi".
+ """))
+ parser.add_argument("module",
+ help="The full path name of an importable module binary (.pyd, .so)") # noqa E:128
+ parser.add_argument("--quiet", action="store_true", help="Run quietly")
+ parser.add_argument("--outpath",
+ help="the output directory (default = location of module binary)") # noqa E:128
+ options = parser.parse_args()
+ module = options.module
+ outpath = options.outpath
+
+ qtest_env = os.environ.get("QTEST_ENVIRONMENT", "")
+ logging.basicConfig(level=logging.DEBUG if qtest_env else logging.INFO)
+ logger = logging.getLogger("pyi_generator")
+
+ if outpath and not Path(outpath).exists():
+ os.makedirs(outpath)
+ logger.info(f"+++ Created path {outpath}")
+ options._pyside_call = False
+ options.is_ci = qtest_env == "ci"
+
+ options.logger = logger
+ generate_pyi(module, outpath, options=options)
+
+
+if __name__ == "__main__":
+ main()
+# eof
diff --git a/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/lib/tool.py b/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/lib/tool.py
new file mode 100644
index 000000000..48546d7cb
--- /dev/null
+++ b/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/lib/tool.py
@@ -0,0 +1,111 @@
+# 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
+from __future__ import annotations
+
+"""
+tool.py
+
+Some useful stuff, see below.
+On the function with_metaclass see the answer from Martijn Pieters on
+https://stackoverflow.com/questions/18513821/python-metaclass-understanding-the-with-metaclass
+"""
+
+from inspect import currentframe
+from textwrap import dedent
+
+
+def build_brace_pattern(level, separators):
+ """
+ Build a brace pattern upto a given depth
+
+ The brace pattern parses any pattern with round, square, curly, or angle
+ brackets. Inside those brackets, any characters are allowed.
+
+ The structure is quite simple and is recursively repeated as needed.
+ When separators are given, the match stops at that separator.
+
+ Reason to use this instead of some Python function:
+ The resulting regex is _very_ fast!
+
+ A faster replacement would be written in C, but this solution is
+ sufficient when the nesting level is not too large.
+
+ Because of the recursive nature of the pattern, the size grows by a factor
+ of 4 at every level, as does the creation time. Up to a level of 6, this
+ is below 10 ms.
+
+ There are other regex engines available which allow recursive patterns,
+ avoiding this problem completely. It might be considered to switch to
+ such an engine if the external module is not a problem.
+ """
+ assert type(separators) is str
+
+ def escape(txt):
+ return "".join("\\" + c for c in txt)
+
+ ro, rc = round_ = "()"
+ so, sc = square = "[]"
+ co, cc = curly = "CD" # noqa E:201 we insert "{}", later...
+ ao, ac = angle = "<>" # noqa E:201
+ q2, bs, q1 = '"', "\\", "'"
+ allpat = round_ + square + curly + angle
+ __ = " "
+ ro, rc, so, sc, co, cc, ao, ac, separators, q2, bs, q1, allpat = map(
+ escape, (ro, rc, so, sc, co, cc, ao, ac, separators, q2, bs, q1, allpat))
+
+ no_brace_sep_q = fr"[^{allpat}{separators}{q2}{bs}{q1}]"
+ no_quot2 = fr"(?: [^{q2}{bs}] | {bs}. )*"
+ no_quot1 = fr"(?: [^{q1}{bs}] | {bs}. )*"
+ pattern = dedent(r"""
+ (
+ (?: {__} {no_brace_sep_q}
+ | {q2} {no_quot2} {q2}
+ | {q1} {no_quot1} {q1}
+ | {ro} {replacer} {rc}
+ | {so} {replacer} {sc}
+ | {co} {replacer} {cc}
+ | {ao} {replacer} {ac}
+ )+
+ )
+ """)
+ no_braces_q = f"[^{allpat}{q2}{bs}{q1}]*"
+ repeated = dedent(r"""
+ {indent} (?: {__} {no_braces_q}
+ {indent} | {q2} {no_quot2} {q2}
+ {indent} | {q1} {no_quot1} {q1}
+ {indent} | {ro} {replacer} {rc}
+ {indent} | {so} {replacer} {sc}
+ {indent} | {co} {replacer} {cc}
+ {indent} | {ao} {replacer} {ac}
+ {indent} )*
+ """)
+ for idx in range(level):
+ pattern = pattern.format(replacer=repeated if idx < level - 1 else no_braces_q,
+ indent=idx * " ", **locals())
+ return pattern.replace("C", "{").replace("D", "}")
+
+
+# Copied from the six module:
+def with_metaclass(meta, *bases):
+ """Create a base class with a metaclass."""
+ # This requires a bit of explanation: the basic idea is to make a dummy
+ # metaclass for one level of class instantiation that replaces itself with
+ # the actual metaclass.
+ class metaclass(type):
+
+ def __new__(cls, name, this_bases, d):
+ return meta(name, bases, d)
+
+ @classmethod
+ def __prepare__(cls, name, this_bases):
+ return meta.__prepare__(name, bases)
+ return type.__new__(metaclass, 'temporary_class', (), {})
+
+
+# A handy tool that shows the current line number and indents.
+def lno(level):
+ lineno = currentframe().f_back.f_lineno
+ spaces = level * " "
+ return f"{lineno}{spaces}"
+
+# eof