Skip to content
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

Documentation missing for fully-functional python class with decorator which overrides __new__() #12276

Open
girishmm opened this issue Apr 14, 2024 · 1 comment

Comments

@girishmm
Copy link

Describe the bug

When generating documentation for class with decorators that override new, documentation for the class is missing and sphinx warns that maximum recursion depth was exceeded in default conf configuration settings.

WARNING: error while formatting arguments for test.Widget: maximum recursion depth exceeded
WARNING: error while formatting signature for test.Widget: Handler <function record_typehints at 0x105504f40> for event 'autodoc-process-signature' threw an exception (exception: maximum recursion depth exceeded)

Generated doc with missing class:

Screenshot 2024-04-14 at 19 10 00

Since the warning warns about an error which formatting signature, if we were to set autodoc_class_signature = "separated", a warning about recursion depth exceeded for __new__().

WARNING: error while formatting arguments for test.Widget.__new__: maximum recursion depth exceeded
WARNING: error while formatting signature for test.Widget.__new__: Handler <function record_typehints at 0x10538cf40> for event 'autodoc-process-signature' threw an exception (exception: maximum recursion depth exceeded)

Generated doc:

Screenshot 2024-04-14 at 19 16 58

To get desired behavior and prevent warnings, one has to manually exclude __init__ and __new__ from the class and explicitly define them using automethod.

Desired doc:

Screenshot 2024-04-14 at 19 00 58
  1. Is this the intended workflow for decorated classes?
  2. Given the class is functional thus implying overridden new() behavior is defined properly, why is the recursion depth exceeded during doc build?

How to Reproduce

This example is from the sqlalchemy recipe for UniqueObject and clearly reproduces this behavior.

test.py

from functools import wraps

from sqlalchemy import Column, Integer, String, create_engine
from sqlalchemy.orm import sessionmaker, scoped_session
from sqlalchemy.ext.declarative import declarative_base


def _unique(session, cls, hashfunc, queryfunc, constructor, arg, kw):
    """This is unique instance retriever."""
    cache = getattr(session, "_unique_cache", None)
    if cache is None:
        session._unique_cache = cache = {}

    key = (cls, hashfunc(*arg, **kw))
    if key in cache:
        return cache[key]
    else:
        with session.no_autoflush:
            q = session.query(cls)
            q = queryfunc(q, *arg, **kw)
            obj = q.first()
            if not obj:
                obj = constructor(*arg, **kw)
                session.add(obj)
        cache[key] = obj
        return obj


def unique_constructor(scoped_session, hashfunc, queryfunc):
    """This is unique decorator."""

    def decorate(cls):
        def _null_init(self, *arg, **kw):
            pass

        @wraps(cls)
        def __new__(cls, bases, *arg, **kw):
            # no-op __new__(), called
            # by the loading procedure
            if not arg and not kw:
                return object.__new__(cls)

            session = scoped_session()

            def constructor(*arg, **kw):
                obj = object.__new__(cls)
                obj._init(*arg, **kw)
                return obj

            return _unique(session, cls, hashfunc, queryfunc, constructor, arg, kw)

        # note: cls must be already mapped for this part to work
        cls._init = cls.__init__
        cls.__init__ = _null_init
        cls.__new__ = classmethod(__new__)
        return cls

    return decorate


Base = declarative_base()

engine = create_engine("sqlite://")

Session = scoped_session(sessionmaker(bind=engine))


@unique_constructor(
    Session, lambda name: name, lambda query, name: query.filter(Widget.name == name)
)
class Widget(Base):
    """This is widget class."""

    __tablename__ = "widget"

    id = Column(Integer, primary_key=True, autoincrement=True)
    name = Column(String, unique=True, nullable=False)

    def __init__(self, name):
        """This is __init__ of widget."""
        self.name = name

    def test(self, num):
        """This is a test method.

        Parameters
        ----------
        num : int
            This is a number.
        """
        pass


Base.metadata.create_all(engine)

w1, w2, w3 = Widget(name="w1"), Widget(name="w2"), Widget(name="w3")
w1b = Widget(name="w1")

assert w1 is w1b
assert w2 is not w3
assert w2 is not w1

Session.commit()

working index.rst

Test
====

.. currentmodule:: test

.. autofunction:: _unique

.. autodecorator:: unique_constructor

.. autoclass:: Widget
	       :exclude-members: __init__, __new__

Environment Information

Platform:              darwin; (macOS-14.3.1-arm64-arm-64bit)
Python version:        3.12.3 (main, Apr  9 2024, 08:09:14) [Clang 15.0.0 (clang-1500.3.9.4)])
Python implementation: CPython
Sphinx version:        7.2.6
Docutils version:      0.20.1
Jinja2 version:        3.1.3
Pygments version:      2.17.2

Sphinx extensions

["sphinx.ext.autodoc", "sphinx.ext.napolean"]

Additional context

This is similar to Issue #12262 spare the recursion depth errors.

@girishmm girishmm changed the title Documentation missing in fully-functional python class with decorator which overrides __new__() Documentation missing for fully-functional python class with decorator which overrides __new__() Apr 14, 2024
@picnixz
Copy link
Member

picnixz commented Apr 16, 2024

I'm tempted to say that we need an extension for SQLAlchemy & co. In other words, I'm not sure I want the code base to work for such cases (I honestly don't know where to start with in order to fix that...).

The thing is that the __init__ and __new__ are kind of special methods and have special handling by the ModuleAnalyzer IIRC, so I'm not really sure how to patch this... Help would be appreciated if someone has time (especially to understand where the recursion error arises).

girishmm added a commit to girishmm/almirah that referenced this issue Apr 22, 2024
When generating documentation for class with decorators that override
new, documentation for the class is missing and sphinx warns that
maximum recursion depth was exceeded in default conf configuration
settings. This is because `__init__` and `__new__` are kind of
special methods and have special handling by the ModuleAnalyzer. So,
the sphinx codebase does not accomodate these by default.

To work around these, changes are needed to how autodoc is processed:

 - Exclude `__init__` and `__new__`
 - Seperate class signature from `__init__`

Reference: sphinx-doc/sphinx#12276
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants