-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add stubs and docstrings for python bindings #196
Comments
Filed as internal issue #145214. |
Reviving this thread. I seem to remember there being Doxygen pages for the Python bindings at some point, but don't remember if or why we internally at Pixar decided not to maintain or publish them? |
Reviving this again. Having code hints is always useful, but it would be particularly useful to have some python docs, especially for Sdf where the python bindings don't match up to the c++ api docs. I've spent a lot of time running dir(SdfClass) to figure out the differences which gets fairly tedious |
Also reviving this one. Apparently Omniverse at least created stubs for usd shipped with their installs. As far as I can tell it's pretty straightforward with stubgen included in mypy. The only thing is required is pointing stubgen to correct .pyd file associated with the module (_sdf.pyd for pxr.Sdf for example) It doesn't resolve the docstring issue, only autocomplete, but even this one improves workflow tremendously: I've also attached stubs to show how exactly it looks like |
Reviving this thread too! Would really love to have proper stubs for this too, and I'm more than happy to help out if needed (when I have some time). For now I'm doing what @igor-elovikov wrote about stubgen, and that seems to work fine. I even created a small script to generate the stubs and copy them into site-package generate_usd_stubs.py. |
Since I created this ticket I've gained a lot more experience creating stubs using mypy's stubgen tool, including for pymel, nuke, and katana, and we also maintain stubs internally for PySide and houdini, which were created using other means. Ultimately, there's not much stopping me (or anyone else) from using mypy's But I don't want to give up on creating a high quality stub package with type information, which will be much more useful for auto-complete and static type analysis. However, I'm stuck on some USD build issue. Read on if you'd like to help out: In order to automatically create stubs with high quality type info, the best way is to embed the signature info into each function docstring, and then use that info during stub generation. I spent some time trying to get USD+boost to include docstrings in its python bindings, but I could not get it to work. There’s a global option that can be enabled to turn on signature generation: https://www.boost.org/doc/libs/1_54_0/libs/python/doc/v2/docstring_options.html The problem is that even with the simplest example taken directly from the boost example code I could not force the signatures to be added to docstrings when the code was built within the USD project, but I could from within a canonical standalone C++ project. I think it has to do with the tf code generation used instead of the standard boost macros. Being perfectly honest, figuring out how to enable the signatures within USD may take more time than I have, but for someone who knows the tf system it may be pretty trivial (hint, hint). So if someone could get this working and expose a CMake flag to enable it, I promise that in return I would solve this stub generation problem for the community, since this is a topic that's come up quite a few times. |
Hello fellow stub lovers! I'd like to reopen this issue. While I understand that this issue has been marked closed, I wouldn't consider it closed until:
What I've triedI have tried the solutions posted here by @igor-elovikov and @hubbas and these are certainly a great start. However, those stubs provide only basic auto-completion and don't pass @staticmethod
def ExportToString(*args, **kwargs) -> None: ... However, the real signature should be something like: @staticmethod
def ExportToString(addSourceFileComment: bool = True) -> str: ... Notice how the Python signature doesn't exactly match the one in the Usd API docs, where this method returns Many methods in the Usd API documentation are more complex and heavily overloaded. But it's possible to define stubs for overloaded methods with the In another example, using the stubs generated by @hubbas's tool, the method @classmethod
def CreateInMemory(cls, *args, **kwargs) -> Any: ... But in reality, this method should return a An appeal to reasonWhat follows is only my opinion based on my experience working as a technical artist in the VFX/games/animation industry. Much of what I'm about to say is based on my own recollections, inferences, and feelings and not much on facts, so please take it with a grain of salt. Also, I'm going to make some broad generalizations about C++ and Python programmers and for that I apologize in advance. So with that out of the way, here goes. When I first started working as a technical artist in California over 15 years ago, there was a palpable cultural divide between C++ programmers and Python programmers. It could be summarized by the adage that "scripting is not programming." As a Python programmer at the time, the view that the tools I was writing were "toys" certainly annoyed me. But years later, after I learned several programming languages besides Python, I understood that the C++ programmers who held this view were, by and large, correct. Python programmers and C++ programmers were not even in the same ballpark in terms of complexity and rigor. Python programmers liked to move fast and break things, while C++ programmers moved deliberately without breaking much. Python programmers thought static types were a waste of time. Why would I need to define the data model of a json file before opening it, when I could just say But things have changed. Over the past 9 years or so, Python has "grown up". Not a full-grown adult with a mortgage, like C++, but certainly old enough to drink. The modern Python programmer wants static types, writes unit tests, and needs an IDE. For example, the 2021 Python Developers Survey states that 74% of Python devs use optional type hinting. The modern Python programmer follows "software engineering best practices" and is used to great tooling. And by the way, all this tooling doesn't yet ship with Python, so the modern Python programmer assembles their own custom toolchain like a sweet kit car. In other words, Python programmers are closer in their mentality to C++ programmers today than they were 15 years ago. Now let's get back to the type stubs. As it stands, So while it's likely that Python programmers in 2008 would have been very happy with the I hope I didn't offend too many people. Although I've made some generalizations, I believe the overall trend of a "grown up" Python is true. I'm CCing @spiffmon, who was very helpful with one of my other issues. Thank you very much for reading. |
Hi @OlegAlexander, In an ideal world, boost python would generate the stubs, since it has all the necessary info (here's the issue for that if you want to add your vote of support: boostorg/python#277 (comment)). For now, I think the best path forward is to build USD with a flag that makes boost python generate signatures in docstrings, which are somewhat passable as a starting point for stub generation. For example, these are the signatures that it generates for the methods you mentioned:
Ignoring the fact that they're not true python signatures, you can see that they reflect some traits which are hard (or impossible?) to determine from parsing the doxygen docs, such as overloaded methods. However, some of the types are missing (notice I'm going to start writing a stub generator based on these signatures in the next few days. I'll post back here when I've made some progress. Building USD with the signatures enabled is pretty straightforward. Here's the diff: diff --git a/.gitignore b/.gitignore
index af6e02bde..55bd2fb82 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,4 @@
.p4*
.DS_Store
.AppleDouble
-
+.*
\ No newline at end of file
diff --git a/cmake/defaults/CXXDefaults.cmake b/cmake/defaults/CXXDefaults.cmake
index e970dfbe2..68a531110 100644
--- a/cmake/defaults/CXXDefaults.cmake
+++ b/cmake/defaults/CXXDefaults.cmake
@@ -42,7 +42,7 @@ _add_define(GL_GLEXT_PROTOTYPES)
_add_define(GLX_GLXEXT_PROTOTYPES)
# Python bindings for tf require this define.
-_add_define(BOOST_PYTHON_NO_PY_SIGNATURES)
+# _add_define(BOOST_PYTHON_NO_PY_SIGNATURES)
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
_add_define(BUILD_OPTLEVEL_DEV)
diff --git a/pxr/base/tf/pyModule.cpp b/pxr/base/tf/pyModule.cpp
index 223cfa7b3..a0b1fcf40 100644
--- a/pxr/base/tf/pyModule.cpp
+++ b/pxr/base/tf/pyModule.cpp
@@ -242,9 +242,12 @@ public:
inline object ReplaceFunctionOnOwner(char const *name, object owner, object fn)
{
+ object fnDocstring = fn.attr("__doc__");
object newFn = DecorateForErrorHandling(name, owner, fn);
PyObject_DelAttrString(owner.ptr(), name);
objects::function::add_to_namespace(owner, name, newFn);
+ // add_to_namespace removes docstrings, so we restore them here
+ newFn.attr("__doc__") = fnDocstring;
return newFn;
}
@@ -429,7 +432,8 @@ void Tf_PyInitWrapModule(
// Disable docstring auto signatures.
boost::python::docstring_options docOpts(true /*show user-defined*/,
- false /*show signatures*/);
+ true /*show py signatures*/,
+ false /*show cpp signatures*/);
// Do the wrapping.
wrapModule(); |
From my experience pybind11 bindings are much better in that regard. mypy method is working pretty much out of the box with correct support of overloads, I guess when usd become "deboostified" and c++17 complaint this could me much easier. Not sure though what exactly the future framework for bindings but pybind seems more robust today. |
@chadrik, thank you so much for working on this! Your approach seems very promising. One thing to keep in mind is how the def prim_exists(stage: Usd.Stage, prim_path: str) -> bool:
"""Does the prim exist on the stage?
The bool operator is overloaded so 'if stage.GetPrimAtPath(primPath)' returns a bool."""
return True if stage.GetPrimAtPath(prim_path) else False I look forward to testing your stubs as soon as they're ready. |
Hi all, Just This is a first draft that I'll be iterating on a lot in the next few weeks, but they're good enough to type check Luma's entire USD code-base using To make these I combined a handful of tools and data sources:
Ultimately, the most correct way to solve this would be to contribute better signature generation to boost-python itself, but this is getting good results and has much more immediate payoff.
@OlegAlexander In your example, |
Hi @chadrik, Thank you so much for creating these stubs! I've had a chance to test them with from pxr import Sdf, Usd, UsdGeom, UsdShade # type: ignore[import] But with the stubs from pxr.UsdGeom import SetStageMetersPerUnit, SetStageUpAxis, Xformable, Mesh
from pxr.Usd import Stage, Prim
from pxr.Sdf import Path as SdfPath # Differentiate from pathlib.Path
from pxr.UsdShade import Material, MaterialBindingAPI This isn't a huge deal, but if there's a simple way to make your stubs work with both import styles, that would be ideal for existing codebases. One other note is about the [mypy-pxr.*]
ignore_errors = true becomes: [[tool.mypy.overrides]]
module = "pxr.*"
ignore_errors = true in the These stubs are total game changer for working with USD. Thank you! |
@OlegAlexander that's interesting, because I did not see this issue on my end using mypy 1.3.0 (though I'm doing more testing), but I did get a report of a similar problem with pylance/pyright. I updated the stubs to 23.5.1, can you try that out? If it doesn't work, open up a ticket at https://github.com/LumaPictures/cg-stubs/issues. |
I'm on from pxr import Sdf, Usd, UsdGeom, UsdShade Thanks! |
…/feature/pipeline_adsk Refine Testing Pipeline
There have been some big strides made recently with static type analysis in python, and I have a perhaps unhealthy obsession with it (hopefully to everyone's benefit). I'm wrapping up work on a script that creates stub files which can be used by an IDE to provide code hints and warnings for the
pxr
python package. I find to be indispensable, especially when working with a new API. I'd like to discuss the possibility of adding the execution of this script as a build option.Python stub files are normal python files with whose class and function definitions contain
pass
statements instead of functionality. There are a handful of extensions used for stub files, depending on the IDE. The ones I'm aware of are .py, .pi, .pypredef. The IDE inspects the stub files instead of the real modules, which is particularly useful when the real modules are compiled extensions and therefore cannot be statically analyzed, such as with USD.Recently there's been a big push in the python community to add static type hints to python, described in PEP484, recently released in the form of the typing module for python 2&3 and actual syntax support in versions 3.5+. Python itself doesn't do anything with the type hints: the point of them is to be available to third-party static type checking tools. The gold standard of these is mypy, but PyCharm also has very good support as of version 2017.1.
PEP484 describes its own type of stub file, saved with a pyi extension. The pyi file sits next to its .py equivalent and in addition to providing membership hints an docstrings like normal stubs, it also adds type hints.
Long story short, my stub generator can create either kind of stub file.
The script works like this:
pxr
module, inspect the contents, looking up argument and type data in the structure from the previous step, and write out .pyi stub files for each__init__.py
file.Note: we rely on inspection to drive stub generation rather than creating stubs directly from Doxygen, because we don't have perfect knowledge of how C++ classes are wrapped in python, and it's better to have missing type info than missing objects/members.
Writing the rules has been challenging and edifying. There's no documentation on the conventions used when wrapping C++ code, so uncovering them has taken some digging (and there's still more to do). My hope is that this tool will ultimately serve as fairly clear documentation of these conventions. Moreover, I think it will reveal where the conventions could be improved or more successfully adhered to, and create a positive feedback loop for developers writing python wrappers.
Here are the rules I've got so far:
There are a handful more of the Sdf mapping types which I've omitted because they have some truly gnarly names like
MapEditProxy_map_string__string__less_string___allocator_pair_string_const__string_____
. It would be nice if we could come up with a simpler set of names and rules for some of these, or possibly make it easier to guess the python name from the C++ type, but in the meantime I can simply document them.Additionally, I've noticed that Doxygen is not creating parameter info for some classes, such as the
UsdStageCacheContext
constructor.Lastly, Alex Mohr mentioned that at Pixar there are docstrings stored in
__DOC
modules which are bound to classes and functions at runtime, but that this script is missing from the repo. We could wrap the generation of those files into the same script that generates stubs, since they both need to parse Doxgyen data.And that's all for now. Thanks for reading.
The text was updated successfully, but these errors were encountered: