diff --git a/cmake/defaults/CXXDefaults.cmake b/cmake/defaults/CXXDefaults.cmake index c59d70c8b7d..9bf805e4aee 100644 --- a/cmake/defaults/CXXDefaults.cmake +++ b/cmake/defaults/CXXDefaults.cmake @@ -42,7 +42,9 @@ _add_define(GL_GLEXT_PROTOTYPES) _add_define(GLX_GLXEXT_PROTOTYPES) # Python bindings for tf require this define. -_add_define(BOOST_PYTHON_NO_PY_SIGNATURES) +if (NOT PXR_BUILD_PYTHON_DOCUMENTATION) + _add_define(BOOST_PYTHON_NO_PY_SIGNATURES) +endif() # Parts of boost (in particular, boost::hash) rely on deprecated features # of the STL that have been removed from some implementations under C++17. diff --git a/docs/python/doxygenlib/cdWriterDocstring.py b/docs/python/doxygenlib/cdWriterDocstring.py index 8ccf771a025..12b98a12d9b 100644 --- a/docs/python/doxygenlib/cdWriterDocstring.py +++ b/docs/python/doxygenlib/cdWriterDocstring.py @@ -523,6 +523,18 @@ def __getPythonObjectAndPath(self, parentPath, overloads): (obj, pypath, jumped) = self.__getPythonObjectByPath(pypath) + if jumped and overloads[0].isFunction(): + # We found a module-level function in our module through a permissive + # search (jumped=True). Do some further vetting. + if obj and hasattr(obj, "__module__") and \ + not parentPath[-1].name.startswith(obj.__module__.split(".")[1]): + # The doxygen object clearly indicates it's from another module. + obj = None + elif type(obj).__module__ != 'Boost.Python': + # The function we found is not a boost function, so it can't correspond + # to our doxygen object + obj = None + # check for the property by either possible name (since there # are two possible naming conventions for boolean properties) ppypath = ppypath1 @@ -687,49 +699,89 @@ def __getFullDoc(self, pyname, pyobj, doxy): # make the doxy element static if it is tagged as such if ATTR_STATIC_METHOD in doxy.doc['tags']: doxy.static = 'yes' - + lines = self.__getShortDescription(pyname, pyobj, doxy) if doxy.isFunction() and type(pyobj) != property: - lines += self.__getSignatureDescription(pyname, pyobj, doxy) - lines.append('') + description = self.__getSignatureDescription(pyname, pyobj, doxy) + if description is not None: + lines += description + lines.append('') lines += self.__getDocumentation(pyname, pyobj, doxy) lines.append('') return lines - def __getOutputFormat(self, pypath, pyobj, overloads): - """Return the line that installs the docstring into the namespace.""" + @classmethod + def __stripBoostSig(cls, doc): + def looksLikeBoostSig(l): + return bool(l and (l[0].isalnum() or l[0] == "_") and ' -> ' in l) + + lines = doc.strip().splitlines() + if len(lines) and looksLikeBoostSig(lines[0]): + # boost signature has been prepended. that means if there is + # an existing description provided in the C++ wrapper it will be + # indented. strip the signature and dedent the description. + found = False + newLines = [] + for line in lines: + if not found and line.startswith(' '): + found = True + if found: + newLines.append(line) + return textwrap.dedent('\n'.join(newLines)) + else: + # None indicates that the existing __doc__ attr should be left as-is + return None + + def __getDocstring(self, pypath, pyobj, overloads): + """Return the docstring.""" - # is there an existing python docstring? we don't want to overwrite - # this because it may be custom authored in the C++ wrap files. - # However, we always override the module doc string for now... + docString = '' + + # boost auto-generates function signatures that can be useful in some + # contexts (such as generating pyi type stubs) because they more + # accurately reflect changes made to the API within the C++ wrap files, + # but for our docstrings we strip them out and replace them with our + # own function signatures. After stripping out the boost signatures, + # if there is a custom authored docstring in the C++ wrap files we honor + # it. if hasattr(pyobj, '__doc__') and pyobj.__doc__ is not None: doc = pyobj.__doc__.strip() - if len(doc) > 0 and not doc.startswith("C++ signature:") \ - and not overloads[0].isModule(): - Debug("Docstring exists for %s - skipping" % pypath) - return None + if len(doc) > 0 and not overloads[0].isModule(): + newDoc = self.__stripBoostSig(doc) + if newDoc is None: + # None indicates that the existing __doc__ attr should be + # left as is + Debug("Docstring exists for %s - skipping" % pypath) + return None + docString = newDoc + + if not docString.strip(): + # get the full docstring that we want to output + lines = [] + pyname = pypath.split('.')[-1] - # get the full docstring that we want to output - lines = [] - pyname = pypath.split('.')[-1] - docString = '' - - if len(overloads) == 1: - lines += self.__getFullDoc(pyname, pyobj, overloads[0]) - if overloads[0].isStatic(): - docString = LABEL_STATIC # set the return type to static - else: - for doxy in overloads: - if doxy.isStatic(): + if len(overloads) == 1: + lines += self.__getFullDoc(pyname, pyobj, overloads[0]) + if overloads[0].isStatic(): docString = LABEL_STATIC # set the return type to static - - desc = self.__getFullDoc(pyname, pyobj, doxy) - if lines and desc: - lines.append('-'*70) - if desc: - lines += desc - docString += '\n'.join(lines) + else: + for doxy in overloads: + if doxy.isStatic(): + docString = LABEL_STATIC # set the return type to static + desc = self.__getFullDoc(pyname, pyobj, doxy) + if lines and desc: + lines.append('-'*70) + if desc: + lines += desc + docString += '\n'.join(lines) + + def __getOutputFormat(self, pypath, pyobj, overloads): + """Return the line that installs the docstring into the namespace.""" + + docString = self.__getDocstring(pypath, pyobj, overloads) + if docString is None: + return None # work out the attribute to set to install this docstring words = pypath.split('.') cls = words[0] diff --git a/pxr/base/tf/__init__.py b/pxr/base/tf/__init__.py index dc2d1225165..a6ddc9610aa 100644 --- a/pxr/base/tf/__init__.py +++ b/pxr/base/tf/__init__.py @@ -29,7 +29,7 @@ # and newer. These interpreters don't search for DLLs in the path anymore, you # have to provide a path explicitly. This re-enables path searching for USD # dependency libraries -import platform, sys +import os, platform, sys if sys.version_info >= (3, 8) and platform.system() == "Windows": import contextlib @@ -109,15 +109,16 @@ def PreparePythonModule(moduleName=None): except KeyError: pass - try: - module = importlib.import_module(".__DOC", f_locals["__name__"]) - module.Execute(f_locals) + if os.environ.get("PXR_USD_PYTHON_DISABLE_DOCS", "false").lower() not in ("1", "true", "yes"): try: - del f_locals["__DOC"] - except KeyError: + module = importlib.import_module(".__DOC", f_locals["__name__"]) + module.Execute(f_locals) + try: + del f_locals["__DOC"] + except KeyError: + pass + except Exception: pass - except Exception: - pass finally: del frame diff --git a/pxr/base/tf/pyModule.cpp b/pxr/base/tf/pyModule.cpp index e35eaa6488f..8172d0fe003 100644 --- a/pxr/base/tf/pyModule.cpp +++ b/pxr/base/tf/pyModule.cpp @@ -242,9 +242,12 @@ class Tf_ModuleProcessor { 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; } @@ -431,7 +434,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();