From 95d8f760e7077f11dff92198a8a0816250de8fe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Pinsard?= Date: Fri, 11 Jun 2021 23:07:07 +0200 Subject: [PATCH 1/9] remove unwanted imports from stubs --- scripts/generate_stubs.py | 48 ++++++++++++++++++++++++++++++++------- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/scripts/generate_stubs.py b/scripts/generate_stubs.py index d6aa832..48c83c2 100644 --- a/scripts/generate_stubs.py +++ b/scripts/generate_stubs.py @@ -1,12 +1,38 @@ -import time +import cmdc + +import inspect import pybind11_stubgen +import time STUBS_LOCATION = "build/cmdc.pyi" +def cleanup_imports(content): + """Remove any classes accidentally imported as modules. + + This is a fix for this bug: + https://github.com/sizmailov/pybind11-stubgen/issues/36 + Some classes with nested classes get imported when they shouldn't, breaking + leaving them breaks autocomplete + """ + + classes = [] + for name, obj in inspect.getmembers(cmdc, inspect.isclass): + classes.append(name) + + # also include any class that might be defined inside of another class. + # these are actually the ones that are causing issues. + for sub_name, _ in inspect.getmembers(obj, inspect.isclass): + classes.append(sub_name) + + for class_name in classes: + content = content.replace("import {}\n".format(class_name), "") + + return content + print("Generating stubs") t0 = time.time() -module = pybind11_stubgen.ModuleStubsGenerator("cmdc") +module = pybind11_stubgen.ModuleStubsGenerator(cmdc) module.write_setup_py = False print("(1) Parsing module..") @@ -17,16 +43,22 @@ print("(1) Finished in {0:0.3} s".format(t1-t0)) print("(1) ----------------------------") -print("(2) Writing stubs..") - -with open(STUBS_LOCATION, "w") as handle: - content = "\n".join(module.to_lines()) +print("(2) Generating stubs content..") - handle.write(content) +content = "\n".join(module.to_lines()) +content = cleanup_imports(content) t2 = time.time() print("(2) Finished in {0:0.3} s".format(t2-t1)) print("(2) ----------------------------") +print("(3) Writing stubs file..") + +with open(STUBS_LOCATION, "w") as handle: + handle.write(content) + +t3 = time.time() +print("(3) Finished in {0:0.3} s".format(t3-t2)) +print("(3) ----------------------------") -print("Succesfully created .{0} in {1:0.3} s".format(STUBS_LOCATION, t2-t0)) +print("Succesfully created .{0} in {1:0.3} s".format(STUBS_LOCATION, t3-t0)) From 744a4e2c979e52feb5428921735519d12768ce4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Pinsard?= Date: Sun, 13 Jun 2021 17:49:39 +0200 Subject: [PATCH 2/9] fail stubs generation in case of degraded signatures. --- scripts/generate_stubs.py | 68 +++++++++++++++++++++++++-------------- 1 file changed, 43 insertions(+), 25 deletions(-) diff --git a/scripts/generate_stubs.py b/scripts/generate_stubs.py index 48c83c2..fe4f2c3 100644 --- a/scripts/generate_stubs.py +++ b/scripts/generate_stubs.py @@ -1,11 +1,17 @@ -import cmdc - import inspect -import pybind11_stubgen +import sys import time +import cmdc +import pybind11_stubgen + STUBS_LOCATION = "build/cmdc.pyi" + +class InvalidSignatureError(Exception): + """Raised when one or more signatures are invalid.""" + + def cleanup_imports(content): """Remove any classes accidentally imported as modules. @@ -29,36 +35,48 @@ def cleanup_imports(content): return content -print("Generating stubs") -t0 = time.time() -module = pybind11_stubgen.ModuleStubsGenerator(cmdc) -module.write_setup_py = False +def main(): + print("Generating stubs") + t0 = time.time() + + module = pybind11_stubgen.ModuleStubsGenerator(cmdc) + module.write_setup_py = False + + print("(1) Parsing module..") + + module.parse() + + invalid_signatures_count = pybind11_stubgen.FunctionSignature.n_invalid_signatures + if invalid_signatures_count > 0: + raise InvalidSignatureError( + f"Module contains {invalid_signatures_count} invalid signature(s)" + ) -print("(1) Parsing module..") + t1 = time.time() + print("(1) Finished in {0:0.3} s".format(t1 - t0)) + print("(1) ----------------------------") -module.parse() + print("(2) Generating stubs content..") -t1 = time.time() -print("(1) Finished in {0:0.3} s".format(t1-t0)) -print("(1) ----------------------------") + content = "\n".join(module.to_lines()) + content = cleanup_imports(content) -print("(2) Generating stubs content..") + t2 = time.time() + print(f"(2) Finished in {t2 - t1:0.3} s") + print("f(2) ----------------------------") -content = "\n".join(module.to_lines()) -content = cleanup_imports(content) + print("(3) Writing stubs file..") -t2 = time.time() -print("(2) Finished in {0:0.3} s".format(t2-t1)) -print("(2) ----------------------------") -print("(3) Writing stubs file..") + with open(STUBS_LOCATION, "w") as handle: + handle.write(content) -with open(STUBS_LOCATION, "w") as handle: - handle.write(content) + t3 = time.time() + print(f"(3) Finished in {t3 - t2:0.3} s") + print("(3) ----------------------------") -t3 = time.time() -print("(3) Finished in {0:0.3} s".format(t3-t2)) -print("(3) ----------------------------") + print(f"Succesfully created .{STUBS_LOCATION} in {t3 - t0:0.3} s") -print("Succesfully created .{0} in {1:0.3} s".format(STUBS_LOCATION, t3-t0)) +if __name__ == "__main__": + main() From 89b4289f7ff50c2d6e8224f141653c2781977e61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Pinsard?= Date: Sun, 13 Jun 2021 20:22:00 +0200 Subject: [PATCH 3/9] fail stubs generation in case of unnamed arguments --- scripts/generate_stubs.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/scripts/generate_stubs.py b/scripts/generate_stubs.py index fe4f2c3..e3c165a 100644 --- a/scripts/generate_stubs.py +++ b/scripts/generate_stubs.py @@ -1,4 +1,5 @@ import inspect +from itertools import count import sys import time @@ -12,6 +13,10 @@ class InvalidSignatureError(Exception): """Raised when one or more signatures are invalid.""" +class UnnamedArgumentError(Exception): + """Raised when one or more signatures contain unnamed arguments.""" + + def cleanup_imports(content): """Remove any classes accidentally imported as modules. @@ -36,6 +41,26 @@ def cleanup_imports(content): return content +def count_unnamed_args(lines): + """Count all the signatures that have unnamed arguments. + + This ignores property setters as these will always have unnamed arguments. + """ + + unnamed_signatures = [] + for line in lines: + if "arg0" in line and "setter" not in previous_line: + unnamed_signatures.append(line) + previous_line = line + + if unnamed_signatures: + print("These signatures contain unnamed arguments:") + for signature in unnamed_signatures: + print(f" {signature.strip(' ')}") + + return len(unnamed_signatures) + + def main(): print("Generating stubs") t0 = time.time() @@ -59,6 +84,14 @@ def main(): print("(2) Generating stubs content..") + lines = module.to_lines() + unnamed_args_count = count_unnamed_args(lines) + + if unnamed_args_count > 0: + raise UnnamedArgumentError( + f"Module contains {unnamed_args_count} signatures with unnamed arguments." + ) + content = "\n".join(module.to_lines()) content = cleanup_imports(content) From 669fc7f635abbac69cc8d55162295874cdca9e09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Pinsard?= Date: Sun, 13 Jun 2021 20:22:42 +0200 Subject: [PATCH 4/9] update contributing.md --- .github/contributing.md | 65 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/.github/contributing.md b/.github/contributing.md index 2fd7226..920300f 100644 --- a/.github/contributing.md +++ b/.github/contributing.md @@ -58,12 +58,49 @@ py::class_(m, "FnDagNode") }) ``` + +### Ensuring good stubs generation. + +To ensure the quality of the generated stubs, here are a few things to keep in mind: +1. There should be no function signature as part of the docstrings. +2. All methods and functions should specify their argument names (and default values) with Pybind's [py::arg](https://pybind11.readthedocs.io/en/stable/basics.html#keyword-arguments) syntax. +In the case where the default value results in an invalid signature, use [py::arg_v](https://pybind11.readthedocs.io/en/stable/advanced/functions.html?highlight=arg_v#default-arguments-revisited) instead. +3. All types should be declared before being used in any signature. +`ForwardDeclarations.inl` is a good place for that as it is included before any other file. Not doing this causes the C++ Types to be used in the signature instead of the Python types, resulting in invalid stubs. + +```c++ +// MSelectionList.inl +SelectionList + .def("add", [](MSelectionList & self, MDagPath object, MObject component = MObject::kNullObj, bool mergeWithExisting = false) -> MSelectionList { + throw std::logic_error{"Function not yet implemented."}; + }, py::arg("object"), + py::arg_v("component", MObject::kNullObj, "Object.kNullObj"), + py::arg("mergeWithExisting") = false, + _doc_SelectionList_add) + +``` + +Which results in the following python signature: +```python +class SelectionList: + def add(self, object: DagPath, component: Object = Object.kNullObj, mergeWithExisting: bool = False) -> SelectionList: ... +``` + +Not using `py::arg` and `py::arg_v` and would have resulted in this invalid signature and the stubs generation would have failed. + +```python +class SelectionList: + def add(self, arg0: DagPath, arg1: Object = , arg2: bool = False) -> SelectionList: ... +# ^--- Invalid Syntax +``` +
### Style Guide - [80 character line width](#80-characters-wide) - [Formatting](formatting) +- [Docstrings](docstrings)
@@ -97,6 +134,32 @@ obj.GoodVariable = false;
+### Docstrings + +Docstrings are defined at the top of the file with `#define` statements with a variable name following this template `_doc_ClassName_methodName` +The actual content of the docstring is placed on the line after the `#define` statement, indented once. + +Here's an example taken from `MDagPath.inl` + +```c++ + +#define _doc_DagPath_exclusiveMatrix \ + "Returns the matrix for all transforms in the path, excluding the\n"\ + "end object." + +#define _doc_DagPath_exclusiveMatrixInverse \ + "Returns the inverse of exclusiveMatrix()." + +#define _doc_DagPath_extendToShape \ + "If the object at the end of this path is a transform and there is a\n"\ + "shape node directly beneath it in the hierarchy, then the path is\n"\ + "extended to that geometry node.\n"\ + "\n"\ + "NOTE: This method will fail if there multiple shapes below the transform."\ +``` + +
+ ### FAQ > Are we aiming to be more consistent with the C++ API or OpenMaya2? @@ -152,4 +215,4 @@ Let's start there. I think it's hard to know until we have enough of a foundatio Yes, the API loves this. It's perfectly happy letting you continue working with bad memory and chooses to randomly crash on you whenever it feels like it instead. This is one of the things I'd like cmdc to get rid of. It's a bad habit. -In this case, if the API isn't returning a bad MStatus, but we know for certain the value is bad, we should throw an exception ourselves to prevent the user from experiencing a crash. \ No newline at end of file +In this case, if the API isn't returning a bad MStatus, but we know for certain the value is bad, we should throw an exception ourselves to prevent the user from experiencing a crash. From 3b4ccd4ddbc3305101836cb6071cbfd988bc6ba4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Pinsard?= Date: Mon, 14 Jun 2021 12:32:10 +0200 Subject: [PATCH 5/9] fix stubs for DagModifier and reformat docstings. --- src/MDagModifier.inl | 66 ++++++++++++++++++++++++-------------------- 1 file changed, 36 insertions(+), 30 deletions(-) diff --git a/src/MDagModifier.inl b/src/MDagModifier.inl index 7af9350..8c457f6 100644 --- a/src/MDagModifier.inl +++ b/src/MDagModifier.inl @@ -1,3 +1,30 @@ +#define _doc_DagModifier_createNode \ + "Adds an operation to the modifier to create a DAG node of the\n"\ + "specified type.\n"\ + "\n"\ + "If a parent DAG node is provided the new node will be parented\n"\ + "under it.\n"\ + "If no parent is provided and the new DAG node is a transform type then\n"\ + "it will be parented under the world.\n"\ + "In both of these cases the method returns the new DAG node.\n"\ + "\n"\ + "If no parent is provided and the new DAG node is not a transform type\n"\ + "then a transform node will be created and the child parented under that.\n"\ + "The new transform will be parented under the world and it is\n"\ + "the transform node which will be returned by the method, not the child.\n"\ + "\n"\ + "None of the newly created nodes will be added to the DAG until\n"\ + "the modifier's doIt() method is called.\n"\ + +#define _doc_DagModifier_reparentNode \ + "Adds an operation to the modifier to reparent a DAG node under\n"\ + "a specified parent.\n"\ + "\n"\ + "If no parent is provided then the DAG node will be reparented under\n"\ + "the world, so long as it is a transform type.\n"\ + "If it is not a transform type then the doIt() will raise a RuntimeError." + + #include "MDGModifier.inl" py::class_(m, "DagModifier") @@ -30,18 +57,9 @@ py::class_(m, "DagModifier") CHECK_STATUS(status) return result; - }, -R"pbdoc(Adds an operation to the modifier to create a DAG node of the specified type. -If a parent DAG node is provided the new node will be parented under it. -If no parent is provided and the new DAG node is a transform type then it will be parented under the world. -In both of these cases, the method returns the new DAG node. - -If no parent is provided and the new DAG node is not a transform type -then a transform node will be created and the child parented under that. -The new transform will be parented under the world \ -and it is the transform node which will be returned by the method, not the child. - -None of the newly created nodes will be added to the DAG until the modifier's doIt() method is called.)pbdoc") + }, py::arg("type"), + py::arg_v("parent", MObject::kNullObj, "Object.kNullObj"), + _doc_DagModifier_createNode) .def("createNode", [](MDagModifier & self, MTypeId typeId, MObject parent = MObject::kNullObj) -> MObject { if (!parent.isNull()) @@ -71,19 +89,9 @@ None of the newly created nodes will be added to the DAG until the modifier's do CHECK_STATUS(status) return result; - }, -R"pbdoc(Adds an operation to the modifier to create a DAG node of the specified type. - -If a parent DAG node is provided the new node will be parented under it. -If no parent is provided and the new DAG node is a transform type then it will be parented under the world. -In both of these cases the method returns the new DAG node. - -If no parent is provided and the new DAG node is not a transform type -then a transform node will be created and the child parented under that. -The new transform will be parented under the world \ -and it is the transform node which will be returned by the method, not the child. - -None of the newly created nodes will be added to the DAG until the modifier's doIt() method is called.)pbdoc") + }, py::arg("typeId"), + py::arg_v("parent", MObject::kNullObj, "Object.kNullObj"), + _doc_DagModifier_createNode) .def("reparentNode", [](MDagModifier & self, MObject node, MObject newParent = MObject::kNullObj) { validate::is_not_null(node, "Cannot reparent a null object."); @@ -140,8 +148,6 @@ None of the newly created nodes will be added to the DAG until the modifier's do MStatus status = self.reparentNode(node, newParent); CHECK_STATUS(status) - }, -R"pbdoc(Adds an operation to the modifier to reparent a DAG node under a specified parent. - -If no parent is provided then the DAG node will be reparented under the world, so long as it is a transform type. -If it is not a transform type then the doIt() will raise a RuntimeError.)pbdoc"); \ No newline at end of file + }, py::arg("node"), + py::arg_v("newParent", MObject::kNullObj, "Object.kNullObj"), + _doc_DagModifier_reparentNode); \ No newline at end of file From 390edd9bc2f2ed10702b5e1ec6ac7205e3d3d31f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Pinsard?= Date: Mon, 14 Jun 2021 20:39:34 +0200 Subject: [PATCH 6/9] readme: add completion and type checking examples --- README.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index adb71f4..01fe51c 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ An alternative set of bindings for the C++ API of Maya 2018-2022. - What if you could address bugs in the bindings yourself? - What if you could *add* missing members yourself? - What if there were bindings for Maya that made it impossible to crash Maya from Python? +- What if setting up code completion and type checking was easy? That's what this repository is for. @@ -54,6 +55,14 @@ print(fn.name()) print("Success") ``` +Code completion working in Visual Studio Code with Pylance: + +https://user-images.githubusercontent.com/3117205/121938610-50b24100-cd4c-11eb-9325-28c35e00f4d7.mp4 + +Type Checking working in Visual Studio Code with Pylance: + +https://user-images.githubusercontent.com/3117205/121938610-50b24100-cd4c-11eb-9325-28c35e00f4d7.mp4 +
### Goal @@ -67,8 +76,8 @@ print("Success") - Object attributes are preferred over rather than set/get methods. For example you can now write array.sizeIncrement=64. - There are more types of exceptions used when methods fail. Not everything is a RuntimeError, as was the case in the old API. - `cmdc` should be faster or as fast as API 2.0 - -> [Reference](https://help.autodesk.com/view/MAYAUL/2020/ENU/?guid=__py_ref_index_html) + > [Reference](https://help.autodesk.com/view/MAYAUL/2020/ENU/?guid=__py_ref_index_html) +- `cmdc` comes with fully type annotated stubs, making it easy to set up code completion as well as type checking.
From ab0fb78a56755b145fda3bc065340c9eb0c46798 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Pinsard?= Date: Mon, 14 Jun 2021 20:57:27 +0200 Subject: [PATCH 7/9] Readme: display video like gifs --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 01fe51c..ec125bb 100644 --- a/README.md +++ b/README.md @@ -57,11 +57,15 @@ print("Success") Code completion working in Visual Studio Code with Pylance: -https://user-images.githubusercontent.com/3117205/121938610-50b24100-cd4c-11eb-9325-28c35e00f4d7.mp4 + Type Checking working in Visual Studio Code with Pylance: -https://user-images.githubusercontent.com/3117205/121938610-50b24100-cd4c-11eb-9325-28c35e00f4d7.mp4 +
From 91c4af184063936ac3109201b31511c765318ba7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Pinsard?= Date: Mon, 14 Jun 2021 21:27:18 +0200 Subject: [PATCH 8/9] readme: use gifs instead of mp4s --- README.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ec125bb..d255b71 100644 --- a/README.md +++ b/README.md @@ -57,15 +57,12 @@ print("Success") Code completion working in Visual Studio Code with Pylance: - +![cmdc_completion](https://user-images.githubusercontent.com/3117205/121947966-f7034400-cd56-11eb-85ad-5b1d9aac091d.gif) + Type Checking working in Visual Studio Code with Pylance: - +![cmdc_type_checking](https://user-images.githubusercontent.com/3117205/121947972-f8347100-cd56-11eb-8782-3d5fcd6c4376.gif)
From df87c0a5a5dfc68d3fee14d21336771c4bec3b1a Mon Sep 17 00:00:00 2001 From: Marcus Ottosson Date: Tue, 15 Jun 2021 07:06:36 +0100 Subject: [PATCH 9/9] Update README.md --- README.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index d255b71..44d8ccd 100644 --- a/README.md +++ b/README.md @@ -55,14 +55,13 @@ print(fn.name()) print("Success") ``` -Code completion working in Visual Studio Code with Pylance: +**Code completion working in Visual Studio Code with Pylance** -![cmdc_completion](https://user-images.githubusercontent.com/3117205/121947966-f7034400-cd56-11eb-85ad-5b1d9aac091d.gif) + +**Type Checking working in Visual Studio Code with Pylance** -Type Checking working in Visual Studio Code with Pylance: - -![cmdc_type_checking](https://user-images.githubusercontent.com/3117205/121947972-f8347100-cd56-11eb-8782-3d5fcd6c4376.gif) +