Search code examples
pythonpyqtread-the-docs

How to correctly setup for PyQt5 imports on readthedocs?


Building the sphinx documentation of a project importing PyQt5 fails (build log) with

QStandardPaths: XDG_RUNTIME_DIR not set, defaulting to '/tmp/runtime-docs'
qt.qpa.screen: QXcbConnection: Could not connect to display 
Could not connect to any X display.

In tox.ini the following was needed:

[testenv:docs]
# avoid QStandardPaths: XDG_RUNTIME_DIR not set
passenv = XDG_RUNTIME_DIR
# xvfb-run prevents Could not connect to any X display
commands = /usr/bin/xvfb-run sphinx-build --color -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html

How to do that on readthedocs ?

This is closely related to PyQt 4 import in read-the-docs, which unfortunately did not contain the error messages. And PyQt5 is installable from pip.

Notes:

  • in the advanced settings, Install your project inside a virtualenv using setup.py install is checked (but unchecking did not help).
  • the reference geoptics snapshot for the following tentatives is f33d233bf67bd7922ec864635e7589e7f4feb40f

Tentatives

1. With mock module

Maybe mocking PyQT5 could work. But this seems a bit cumbersome.

Adapting from this answer, adding

import mock 
MOCK_MODULES = ['sip', 'PyQt5', 'PyQt5.QtGui', 'PyQt5.QtCore', 'PyQt5.QtWidgets']
sys.modules.update((mod_name, mock.MagicMock()) for mod_name in MOCK_MODULES)

to conf.py yields

    class _GRay(GCounterPart, QGraphicsPathItem):
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases

2. With the sphinx built-in autodoc_mock_imports

same error with the simpler (just one line added to conf.py)

autodoc_mock_imports = ['sip', 'PyQt5', 'PyQt5.QtGui', 'PyQt5.QtCore', 'PyQt5.QtWidgets']

3. With custom Mock

Using julen's custom Mock class

class Mock(object):
    def __init__(self, *args, **kwargs):
        pass

    def __call__(self, *args, **kwargs):
        return Mock()

    @classmethod
    def __getattr__(cls, name):
        if name in ('__file__', '__path__'):
            return '/dev/null'
        elif name[0] == name[0].upper():
            mockType = type(name, (), {})
            mockType.__module__ = __name__
            return mockType
        else:
            return Mock()

MOCK_MODULES = ['sip', 'PyQt5', 'PyQt5.QtGui', 'PyQt5.QtCore', 'PyQt5.QtWidgets']
for mod_name in MOCK_MODULES:
    sys.modules[mod_name] = Mock()

yields

  File ".../geoptics/guis/qt/main.py", line 59, in <module>
    app = QCoreApplication.instance()
AttributeError: type object 'QCoreApplication' has no attribute 'instance'

It should be possible to move the app definition/retrieval stuff from the module level to a function body, not executed at module import.

4. autodoc_mock_imports without multiple inheritance

autodoc_mock_imports = ['sip', 'PyQt5', 'PyQt5.QtGui', 'PyQt5.QtCore', 'PyQt5.QtWidgets']

in conf.py, as in the 2nd tentative, but multiple inheritance replaced with decorators. The changes are described in this pull request.

Now the error is

geoptics.guis.qt.handles.LineHandle.reset_move:1:term not in glossary: move restrictions

because the geoptics class _GScene(QGraphicsScene) where the term is defined has been mocked away by sphinx, and its documentation lost.


Comments left in relevant issues of:


Solution

  • The autodoc_mock_imports has been fixed in sphinx-1.7.5.

    In docs/conf.py add the following line:

    autodoc_mock_imports = ['sip', 'PyQt5', 'PyQt5.QtGui', 'PyQt5.QtCore', 'PyQt5.QtWidgets']
    

    Then, create a docs/requirements.txt with a single line

    sphinx>=1.7.5
    

    and declare docs/requirements.txt in the readthedocs project admin>advanced settings>Requirements file.

    Happily, this does not bypass the setup.py, it just adds the sphinx-1.7.5 version requirement.