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

Bug: incorrect function is_singledispatch_method and bugs with classmethod decorator #11531

Closed
jeertmans opened this issue Jul 28, 2023 · 5 comments · Fixed by #11284
Closed

Comments

@jeertmans
Copy link

jeertmans commented Jul 28, 2023

Describe the bug

The sphinx.util.inspect.is_singledispatch_method appears to be incorrect.

def is_singledispatch_method(obj: Any) -> bool:
"""Check if the object is singledispatch method."""
return isinstance(obj, singledispatchmethod)

Even though the decorator is defined as a class, using isinstance will never work (see example below).

singledispatchmethod definition: https://github.com/python/cpython/blob/a9e5e59b7d912b4a90c550fad6a2d208bf8a1ed9/Lib/functools.py#L902C3-L935

How to Reproduce

The following code fails.

from functools import singledispatchmethod
from sphinx.util.inspect import is_singledispatch_method

class Negator:
    @singledispatchmethod
    @classmethod
    def neg(cls, arg):
        """HELP"""
        raise NotImplementedError("Cannot negate a")

    @neg.register
    @classmethod
    def _(cls, arg: int):
        return -arg

    @neg.register
    @classmethod
    def _(cls, arg: bool):
        return not arg

assert is_singledispatch_method(Negator.neg)  # This fails, with or without @classmethod

Environment Information

Using the latest version (master branch).

When combined with `@classmethod`, this is what happens:


reading sources... [100%] reference/index                                                                
/home/jeertmans/repositories/DiffeRT/DiffeRT2d/differt2d/scene.py:docstring of functools.singledispatchmethod.__get__.<locals>._method:14: ERROR: Unexpected indentation.
looking for now-outdated files... none found   

The error comes from the fact the obj.__wrapped.__doc__ is classmethod's documentation, and not the original function documentation. To obtain such information, you need to get obj.__wrapped..__func__.__doc__



### Sphinx extensions

```python
`["sphinx.ext.autodoc"]`

Additional context

I could patch this issue with following preprocessor (defined in conf.py):

from sphinx.util.inspect import isclassmethod

def is_singledispatch_classmethod(obj):
    return hasattr(obj, "register") and isclassmethod(getattr(obj, "__wrapped__"), None)


def patch_singledispatch_classmethod_signature(app, obj, bound_method):
    if bound_method and is_singledispatch_classmethod(obj):
        original = obj.__wrapped__.__func__
        obj.__wrapped__ = original
        obj.__annotations__ = original.__annotations__
        obj.__doc__ = original.__doc__


def setup(app):
    app.connect(
        "autodoc-before-process-signature",
        patch_singledispatch_classmethod_signature,
    )

Related issues: #11278?

@picnixz
Copy link
Member

picnixz commented Jul 29, 2023

Yes, this is likely related to #11278. I remember that one particularity is because of
(see PR #11284):

If a method is decorated with @singledispatchmethod and @classmethod, the MethodDocumenter miserably fails to extract the correct signature or docstring. However, in order to properly extract the signature or the docstring, this would require a lot of changes in the sphinx.util.inspect or sphinx.ext.autodoc module which may break other things.

Since we are dropping Python 3.8 support, I'd gladly fix this for the next release (when I'm done with the other tasks). Actually, the reason why is_singledispatch_method fails is because the object being documented is not the instance of the descriptor. More precisely, the object being documented is class.meth (which triggers the descriptor and then you get the _method constructed in __get__) and not class.__dict__['meth'] which would be an instance of singledispatchmethod.

I think I will try to see which approach is actually the best. Maybe I'll need to rethink the logic of my current PR.

@jeertmans
Copy link
Author

Why is this related to dropping Python3.8? @picnixz

But thanks for taking care of that :-)

@picnixz
Copy link
Member

picnixz commented Aug 2, 2023

On our side, the __doc__ of a singledispatched classmethod is different in Python 3.8 and 3.9 (see the differences https://github.com/sphinx-doc/sphinx/pull/11284/files#diff-d5e4a37fa69544387ceb74efa12b572152ebb92b8d207fbecc82f76d0c1aae18R2155-R2186). In Python 3.8, the docstring is actually the one from the classmethod and not the docstring of the decorated function. Also, iirc, the implementation of singledispatchmethod changed between 3.8 and 3.9 (and 3.10 perhaps). So by dropping 3.8, I can avoid some corner cases (and we could fix both issues at the same time).

@jeertmans
Copy link
Author

Ok good to know :-) This was not documented in functools to my knowledge, glad you found that!

@picnixz
Copy link
Member

picnixz commented Aug 3, 2023

So here's an update:

  • sphinx.util.inspect.is_singledispatchmethod is correct. You can see the same behaviour with a simple classmethod decorator:

    class A:
    
        @classmethod
        def a(cls): ...
    
    assert not isinstance(A.a, classmethod) 
    assert isinstance(A.__dict__['a'], classmethod)

    Writing A.a triggers the descriptor and you'll end up with the bounded method and not the instance of a classmethod. So there is no need for changing is_singledispatchmethod since it works as intended.

  • What is actually wrong is the fact that autodoc incorrectly recovers the underlying object and so, what you are documenting is no more the singledispatchmethod instance but the underlying one. So we simply need to fix it at the autodoc level.

@AA-Turner AA-Turner added this to the some future version milestone Aug 11, 2023
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Apr 14, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants