Sphinx: Filtering versionadded/versionchanged Noise on Inherited Members

With inherited members enabled, Sphinx autodoc copies them onto a subclass’s page – including any .. versionadded:: / .. versionchanged:: notes in the base-class docstrings. The problem here is that this version information is about the superclass, but our user-facing docs are about the subclass, and that subclass may be newer than the superclass.

Imagine if the whole class that’s being documented was introduced in version 3.11, but we have “Changed in version 3.9” on some inherited members – that’s confusing, isn’t it? You’d wonder whether the class really was new in version 3.11 or whether you could trust the docs at all. On the pages of older sibling classes, the same notes are useful; that’s why we annotated them in the first place.

My fix is a per-page threshold. Each affected page declares the version it first appeared in, as a docinfo field at the very top of the .rst file:

:min-version: 3.11

Page Title
==========

.. autoclass:: mypackage.BananaCounter
   :members:
   :inherited-members:

The :inherited-members: flag is what pulls the base-class members – and their version notes – onto this page in the first place; without it there’d be no noise to filter. A doctree-read hook in conf.py then drops version notes at or below that threshold, but only inside autodoc member descriptions:

def filter_version_notes(app, doctree):
    from packaging.version import parse as parse_version
    from sphinx import addnodes

    threshold = app.env.metadata.get(app.env.docname, {}).get("min-version")
    if not threshold:
        return
    threshold = parse_version(threshold)
    for node in list(doctree.findall(addnodes.versionmodified)):
        if node["type"] == "deprecated" or parse_version(node["version"]) > threshold:
            continue
        ancestor = node.parent
        while ancestor is not None and not isinstance(ancestor, addnodes.desc):
            ancestor = ancestor.parent
        if ancestor is not None:  # inside an autodoc member; not a page-level note
            node.parent.remove(node)


def setup(app):
    app.connect("doctree-read", filter_version_notes)

Notes on the details: