diff options
author | Christian Tismer <tismer@stackless.com> | 2021-05-17 16:48:17 +0200 |
---|---|---|
committer | Qt Cherry-pick Bot <cherrypick_bot@qt-project.org> | 2021-05-21 10:16:30 +0000 |
commit | a78567ab592bd2f4b87c1e2870335f4ec20675a6 (patch) | |
tree | 7211d2cd627cb37fb4546e53d09e09a08687abd3 | |
parent | 147d5a7e06c5827e5ccedf03b55d9e2edb7d3e67 (diff) |
__feature__: Support generation of modified .pyi files
We add a possibility to select features before generating
the pyi files. This will improve the feature adoption, because
common IDEs can use these changed .pyi files.
- avoid crashes because of early selection
- add an option to generate_pyi
- document pyside6-genpyi and shiboken6-genpyi
- build a correct display of properties
Task-number: PYSIDE-1019
Change-Id: Ib75dfcbaccaa969ef47eeb142d9c034a2a6c11d6
Reviewed-by: Friedemann Kleint <Friedemann.Kleint@qt.io>
(cherry picked from commit d84d5859906fd4331d5d7814815bb58d32c988a6)
Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org>
8 files changed, 142 insertions, 29 deletions
diff --git a/sources/pyside6/PySide6/support/generate_pyi.py b/sources/pyside6/PySide6/support/generate_pyi.py index 74ef2acd7..7e1c52ddd 100644 --- a/sources/pyside6/PySide6/support/generate_pyi.py +++ b/sources/pyside6/PySide6/support/generate_pyi.py @@ -84,6 +84,8 @@ def generate_all_pyi(outpath, options): # Perhaps this can be automated? PySide6.support.signature.mapping.USE_PEP563 = USE_PEP563 + import __feature__ as feature + outpath = Path(outpath) if outpath and os.fspath(outpath) else Path(PySide6.__file__).parent name_list = PySide6.__all__ if options.modules == ["all"] else options.modules errors = ", ".join(set(name_list) - set(PySide6.__all__)) @@ -97,7 +99,9 @@ def generate_all_pyi(outpath, options): name_list = [quirk1, quirk2] for mod_name in name_list: import_name = "PySide6." + mod_name - generate_pyi(import_name, outpath, options) + feature_id = feature.get_select_id(options.feature) + with feature.force_selection(feature_id, import_name): + generate_pyi(import_name, outpath, options) if __name__ == "__main__": @@ -111,6 +115,8 @@ if __name__ == "__main__": help="the output directory (default = binary location)") parser.add_argument("--sys-path", nargs="+", help="a list of strings prepended to sys.path") + parser.add_argument("--feature", nargs="+", choices=["snake_case", "true_property"], default=[], + help="""a list of feature names. Example: `--feature snake_case true_property`""") options = parser.parse_args() if options.quiet: logger.setLevel(logging.WARNING) diff --git a/sources/pyside6/doc/feature-why.rst b/sources/pyside6/doc/feature-why.rst index 3f11c6131..b95cb77ac 100644 --- a/sources/pyside6/doc/feature-why.rst +++ b/sources/pyside6/doc/feature-why.rst @@ -244,4 +244,21 @@ see the Python documentation on `Import-Hooks`_. If you would like to modify ``__import__`` anyway without destroying the features, please override just the ``__orig_import__`` function. + +IDEs and Modifying Python stub files +------------------------------------ + +|project| comes with pre-generated ``.pyi`` stub files in the same location as +the binary module. For instance, in the site-packages directory, you can find +a ``QtCore.pyi`` file next to ``QtCore.abi3.so`` or ``QtCore.pyd`` on Windows. + +When using ``__feature__`` often with common IDEs, you may want to provide +a feature-aware version of ``.pyi`` files to get a correct display. The simplest +way to change them all in-place is the command + +.. code-block:: python + + pyside6-genpyi all --feature snake_case true_property + + .. _`Import-Hooks`: https://docs.python.org/3/reference/import.html#import-hooks diff --git a/sources/shiboken6/libshiboken/basewrapper.cpp b/sources/shiboken6/libshiboken/basewrapper.cpp index a467abbc6..7cb3f25b9 100644 --- a/sources/shiboken6/libshiboken/basewrapper.cpp +++ b/sources/shiboken6/libshiboken/basewrapper.cpp @@ -557,17 +557,29 @@ int SbkObjectType_GetReserved(PyTypeObject *type) void SbkObjectType_SetReserved(PyTypeObject *type, int value) { - PepType_SOTP(reinterpret_cast<SbkObjectType *>(type))->pyside_reserved_bits = value; + auto ptr = PepType_SOTP(reinterpret_cast<SbkObjectType *>(type)); + // PYSIDE-1019: During import PepType_SOTP is still zero. + if (ptr == nullptr) + return; + ptr->pyside_reserved_bits = value; } const char **SbkObjectType_GetPropertyStrings(PyTypeObject *type) { - return PepType_SOTP(type)->propertyStrings; + auto ptr = PepType_SOTP(reinterpret_cast<SbkObjectType *>(type)); + // PYSIDE-1019: During import PepType_SOTP is still zero. + if (ptr == nullptr) + return nullptr; + return ptr->propertyStrings; } void SbkObjectType_SetPropertyStrings(PyTypeObject *type, const char **strings) { - PepType_SOTP(reinterpret_cast<SbkObjectType *>(type))->propertyStrings = strings; + auto ptr = PepType_SOTP(reinterpret_cast<SbkObjectType *>(type)); + // PYSIDE-1019: During import PepType_SOTP is still zero. + if (ptr == nullptr) + return; + ptr->propertyStrings = strings; } // diff --git a/sources/shiboken6/libshiboken/signature/signature_doc.rst b/sources/shiboken6/libshiboken/signature/signature_doc.rst index 70fbda714..0fb26ae52 100644 --- a/sources/shiboken6/libshiboken/signature/signature_doc.rst +++ b/sources/shiboken6/libshiboken/signature/signature_doc.rst @@ -180,7 +180,7 @@ layout.py As more applications used the signature module, different formatting of signatures was needed. To support that, we created the function ``create_signature``, which -has a parameter to choose from some prefefined layouts. +has a parameter to choose from some predefined layouts. *typing27.py* @@ -326,6 +326,15 @@ not only signatures but also constants and enums of all PySide modules. This serves as an extra challenge that has a very positive effect on the completeness and correctness of signatures. +The module has a ``--feature`` option to generate modified .pyi files. +A shortcut for this command is ``pyside6-genpyi``. + +A useful command to change all .pyi files to use all features is + +.. code-block:: python + + pyside6-genpyi all --feature snake_case true_property + pyi_generator.py ---------------- @@ -334,6 +343,8 @@ pyi_generator.py has been extracted from ``generate_pyi.py``. It allows the generation of ``.pyi`` files from arbitrary extension modules created with shiboken. +A shortcut for this command is ``shiboken6-genpyi``. + Current Extensions ------------------ diff --git a/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/feature.py b/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/feature.py index b6d6657fe..8776c7de9 100644 --- a/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/feature.py +++ b/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/feature.py @@ -55,6 +55,7 @@ The select id `-1` has the spectial meaning "ignore this module". """ import sys +from contextlib import contextmanager all_feature_names = [ "snake_case", @@ -128,12 +129,7 @@ def feature_import(name, *args, **kwargs): # This is an `import from` statement that corresponds to `IMPORT_NAME`. # The following `IMPORT_FROM` will handle errors. (Confusing, ofc.) - flag = 0 - for feature in args[2]: - if feature in _really_all_feature_names: - flag |= globals()[feature] - else: - raise SyntaxError(f"PySide feature {feature} is not defined") + flag = get_select_id(args[2]) flag |= existing & 255 if isinstance(existing, int) and existing >= 0 else 0 pyside_feature_dict[importing_module] = flag @@ -198,4 +194,35 @@ def _current_selection(flag): names.append(name) return names + +def get_select_id(feature_names): + flag = 0 + for feature in feature_names: + if feature in _really_all_feature_names: + flag |= globals()[feature] + else: + raise SyntaxError(f"PySide feature {feature} is not defined") + return flag + + +@contextmanager +def force_selection(select_id, mod_name): + """ + This function is for generating pyi files with features. + The selection id is set globally after performing the unswitched + import. + + """ + __init__() + saved_feature_dict = pyside_feature_dict.copy() + for name in pyside_feature_dict: + set_selection(0, name) + __import__(mod_name) + for name in pyside_feature_dict.copy(): + set_selection(select_id, name) + try: + yield + finally: + pyside_feature_dict.update(saved_feature_dict) + #eof 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 index a71ed63f9..c9c741261 100644 --- a/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/lib/enum_sig.py +++ b/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/lib/enum_sig.py @@ -50,7 +50,9 @@ by producing a lot of clarity. import inspect import sys import types +import typing from shibokensupport.signature import get_signature as get_sig +from shibokensupport.signature.layout import create_signature class ExactEnumerator(object): @@ -128,6 +130,7 @@ class ExactEnumerator(object): subclasses = [] functions = [] enums = [] + properties = [] for thing_name, thing in class_members: if inspect.isclass(thing): @@ -135,16 +138,20 @@ class ExactEnumerator(object): subclasses.append((subclass_name, thing)) elif inspect.isroutine(thing): func_name = thing_name.split(".")[0] # remove ".overload" - signature = getattr(thing, "__signature__", None) - if signature is not None: - functions.append((func_name, thing)) + functions.append((func_name, thing)) elif type(type(thing)) is EnumMeta: # take the real enum name, not what is in the dict enums.append((thing_name, type(thing).__qualname__, thing)) + elif isinstance(thing, property): + properties.append((thing_name, thing)) + init_signature = getattr(klass, "__signature__", None) enums.sort(key=lambda tup: tup[1 : 3]) # sort by class then enum value self.fmt.have_body = bool(subclasses or functions or enums or init_signature) + # We want to handle functions and properties together. + func_prop = sorted(functions + properties) + with self.fmt.klass(class_name, class_str): self.fmt.level += 1 self.fmt.class_name = class_name @@ -161,11 +168,14 @@ class ExactEnumerator(object): if len(subclasses): self.section() ret.update(self.function("__init__", klass)) - for func_name, func in functions: + for func_name, func in func_prop: if func_name != "__init__": - ret.update(self.function(func_name, func)) + 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(functions): + if len(func_prop): self.section() return ret @@ -173,16 +183,24 @@ class ExactEnumerator(object): def get_signature(func): return func.__signature__ - def function(self, func_name, func): + def function(self, func_name, func, decorator=None): self.func = func # for is_method() ret = self.result_type() - signature = self.get_signature(func) + signature = self.get_signature(func, decorator) if signature is not None: - with self.fmt.function(func_name, signature) as key: + 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): @@ -222,6 +240,24 @@ class HintingEnumerator(ExactEnumerator): hinting stubs. Only default values are replaced by "...". """ - @staticmethod - def get_signature(func): - return get_sig(func, "hintingstub") + 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 decorator and not sig: + 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 index e95b6e959..4adf2fb01 100644 --- a/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/lib/pyi_generator.py +++ b/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/lib/pyi_generator.py @@ -142,6 +142,7 @@ class Formatter(Writer): def module(self, mod_name): self.mod_name = mod_name support = "PySide6.support" if self.options._pyside_call else "shibokensupport" + extra = "from PySide6 import PyClassProperty" if self.options._pyside_call else "" txt = f"""\ # Module `{mod_name}` @@ -150,6 +151,7 @@ class Formatter(Writer): from shiboken6 import Shiboken from {support}.signature.mapping import ( Virtual, Missing, Invalid, Default, Instance) + {extra} """ self.print(dedent(txt)) # This line will be replaced by the missing imports postprocess. @@ -170,7 +172,7 @@ class Formatter(Writer): yield @contextmanager - def function(self, func_name, signature): + def function(self, func_name, signature, decorator=None): if func_name == "__init__": self.print() key = func_name @@ -180,14 +182,17 @@ class Formatter(Writer): self.print(f'{spaces}@typing.overload') self._function(func_name, sig, spaces) else: - self._function(func_name, signature, spaces) + self._function(func_name, signature, spaces, decorator) if func_name == "__init__": self.print() yield key - def _function(self, func_name, signature, spaces): + def _function(self, func_name, signature, spaces, decorator=None): + if decorator: + self.print(f'{spaces}@{decorator}') if self.is_method() and "self" not in signature.parameters: - self.print(f'{spaces}@staticmethod') + 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}: ...') @@ -295,7 +300,6 @@ def generate_pyi(import_name, outpath, options): subprocess.check_output([sys.executable, outfilepath]) - if __name__ == "__main__": parser = argparse.ArgumentParser( formatter_class=argparse.RawDescriptionHelpFormatter, diff --git a/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/mapping.py b/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/mapping.py index 9ba40679e..f4b3272b5 100644 --- a/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/mapping.py +++ b/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/mapping.py @@ -232,6 +232,7 @@ type_map.update({ "int": int, "List": ArrayLikeVariable, "long": int, + "nullptr": None, "PyCallable": typing.Callable, "PyObject": object, "PyArrayObject": ArrayLikeVariable, # numpy @@ -386,7 +387,6 @@ def init_sample(): "Foo.HANDLE": int, "HANDLE": int, "Null": None, - "nullptr": None, "ObjectType.Identifier": Missing("sample.ObjectType.Identifier"), "OddBool": bool, "PStr": str, |