diff options
Diffstat (limited to 'sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/lib')
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 |