重构模块并保持向后兼容性,包括 intersphinx

Refactoring a module and keeping backward compatibility, including for intersphinx

给定一个 python 包 pack 提供 pack.foo.Bar class:

pack/
    __init__.py  # empty
    foo.py
        # content of foo.py
        """
        This module does stuff using the :class:`pack.foo.Bar` class.
        """

        class Bar(object):
            pass

        # much more code here

我想将 pack.foo 模块重构为一个包,以便将 Bar class 移动到 pack/foo/bar.py 文件中。为了保持向后兼容性,我可以把这个放到 pack/foo/__init__.py 文件中:

"""
This module does stuff using the :class:`pack.foo.Bar` class.
"""

from pack.foo.bar import Bar
__all__ = ['Bar']

API 的用户仍然可以使用 from pack.foo import Bar

仍然存在一个问题:使用 sphinx 时的引用。 sphinx解析pack/foo/__init__.py中的docstring时,找不到目标:

WARNING: py:class reference target not found pack.foo.bar.Bar

这会破坏用户在使用 intersphinx 扩展时制作的文档。

重构包结构并仍然保持完全向后兼容性(包括 sphinx 对象清单)的正确方法是什么?

这是我自己的一些发现的答案。

在这种情况下没有灵丹妙药。

首先,sphinx-apidoc 生成的代码文档将具有从文件布局推断出的模块布局。这意味着 pack/foo.py 中定义的 Bar class 将被记录为 pack.foo.Bar,无论 pack/__init__.py.

中发生什么导入处理

其次,autodoc extension仍然可以使用。 Autodoc 只是尝试正常导入文档符号,因为它们在重组文本中定义。这样,您可以使用

Bar class 生成 HTML 文档
.. autoclass:: pack.Bar
    :members:

不过有一个问题。任何记录的符号(以及它们的每个依赖项,传递性)必须与旨在记录的完全相同的命名空间一起使用。考虑我们示例的变体,提供额外的 class Baz:

pack/
    __init__.py
        # content of __init__.py
        from pack.foo.bar import Bar, Baz
        __all__ = ['Bar', 'Baz']

    foo.py
        # content of foo.py
        """
        This module does stuff using the :class:`pack.foo.Bar` class.
        """

        class Bar(object):
            pass

        class Baz(Bar):  # Here, sphinx thinks that Baz inherits from
            pass         # pack.foo.Bar because Bar.__module__ is
                         # pack.foo in this context.

Sphinx 将无法导入 pack.foo.Bar,因为由于 pack/__init__.py.

的内容,它只能作为 pack.Bar 导入

为了完成这项工作,必须找到一种方法,在 API 的代码本身中只使用包的 API 提供的精确导入布局。例如,在我们的示例中,这可以通过在单独的文件中定义 BarBaz class 来实现。祝你好运,小心循环导入!