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

Close #5603: Autodoc: add :canonical: for everything #9039

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 14 additions & 12 deletions doc/usage/advanced/websupport/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@
Web Support Quick Start
=======================

.. py:currentmodule:: sphinxcontrib.websupport

Building Documentation Data
----------------------------

To make use of the web support package in your application you'll need to build
the data it uses. This data includes pickle files representing documents,
search indices, and node data that is used to track where comments and other
things are in a document. To do this you will need to create an instance of the
:class:`~.WebSupport` class and call its :meth:`~.WebSupport.build` method::
:class:`~WebSupport` class and call its :meth:`~WebSupport.build` method::

from sphinxcontrib.websupport import WebSupport

Expand All @@ -30,22 +32,22 @@ called "static" and contains static files that should be served from "/static".

If you wish to serve static files from a path other than "/static", you can
do so by providing the *staticdir* keyword argument when creating the
:class:`~.WebSupport` object.
:class:`~WebSupport` object.


Integrating Sphinx Documents Into Your Webapp
----------------------------------------------

Now that the data is built, it's time to do something useful with it. Start off
by creating a :class:`~.WebSupport` object for your application::
by creating a :class:`~WebSupport` object for your application::

from sphinxcontrib.websupport import WebSupport

support = WebSupport(datadir='/path/to/the/data',
search='xapian')

You'll only need one of these for each set of documentation you will be working
with. You can then call its :meth:`~.WebSupport.get_document` method to access
with. You can then call its :meth:`~WebSupport.get_document` method to access
individual documents::

contents = support.get_document('contents')
Expand Down Expand Up @@ -101,7 +103,7 @@ Authentication
To use certain features such as voting, it must be possible to authenticate
users. The details of the authentication are left to your application. Once a
user has been authenticated you can pass the user's details to certain
:class:`~.WebSupport` methods using the *username* and *moderator* keyword
:class:`~WebSupport` methods using the *username* and *moderator* keyword
arguments. The web support package will store the username with comments and
votes. The only caveat is that if you allow users to change their username you
must update the websupport package's data::
Expand Down Expand Up @@ -130,7 +132,7 @@ whether a user is logged in and then retrieves a document is::
The first thing to notice is that the *docname* is just the request path. This
makes accessing the correct document easy from a single view. If the user is
authenticated, then the username and moderation status are passed along with the
docname to :meth:`~.WebSupport.get_document`. The web support package will then
docname to :meth:`~WebSupport.get_document`. The web support package will then
add this data to the ``COMMENT_OPTIONS`` that are used in the template.

.. note::
Expand Down Expand Up @@ -162,8 +164,8 @@ would be like this::
return render_template('doc.html', document=document)

Note that we used the same template to render our search results as we did to
render our documents. That's because :meth:`~.WebSupport.get_search_results`
returns a context dict in the same format that :meth:`~.WebSupport.get_document`
render our documents. That's because :meth:`~WebSupport.get_search_results`
returns a context dict in the same format that :meth:`~WebSupport.get_document`
does.


Expand All @@ -173,7 +175,7 @@ Comments & Proposals
Now that this is done it's time to define the functions that handle the AJAX
calls from the script. You will need three functions. The first function is
used to add a new comment, and will call the web support method
:meth:`~.WebSupport.add_comment`::
:meth:`~WebSupport.add_comment`::

@app.route('/docs/add_comment', methods=['POST'])
def add_comment():
Expand Down Expand Up @@ -202,7 +204,7 @@ specific node, and is aptly named
data = support.get_data(node_id, username, moderator)
return jsonify(**data)

The final function that is needed will call :meth:`~.WebSupport.process_vote`,
The final function that is needed will call :meth:`~WebSupport.process_vote`,
and will handle user votes on comments::

@app.route('/docs/process_vote', methods=['POST'])
Expand All @@ -220,7 +222,7 @@ and will handle user votes on comments::
Comment Moderation
------------------

By default, all comments added through :meth:`~.WebSupport.add_comment` are
By default, all comments added through :meth:`~WebSupport.add_comment` are
automatically displayed. If you wish to have some form of moderation, you can
pass the ``displayed`` keyword argument::

Expand All @@ -243,7 +245,7 @@ displayed::
Rejecting comments happens via comment deletion.

To perform a custom action (such as emailing a moderator) when a new comment is
added but not displayed, you can pass callable to the :class:`~.WebSupport`
added but not displayed, you can pass callable to the :class:`~WebSupport`
class when instantiating your support object::

def moderation_callback(comment):
Expand Down
56 changes: 36 additions & 20 deletions sphinx/ext/autodoc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1133,7 +1133,38 @@
return super().sort_members(documenters, order)


class ModuleLevelDocumenter(Documenter):
class PyObjectDocumenter(Documenter):
"""Documenter for eveything except modules"""

def add_directive_header(self, sig: str) -> None:
super().add_directive_header(sig)
self.add_canonical_option()

def add_canonical_option(self) -> None:
sourcename = self.get_sourcename()

canonical_fullname = self.get_canonical_fullname()
if canonical_fullname and self.fullname != canonical_fullname:
self.add_line(' :canonical: %s' % canonical_fullname, sourcename)

def get_canonical_fullname(self) -> Optional[str]:

Check failure on line 1150 in sphinx/ext/autodoc/__init__.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (F821)

sphinx/ext/autodoc/__init__.py:1150:41: F821 Undefined name `Optional`
modname = safe_getattr(self.object, '__module__', self.modname)
qualname = safe_getattr(self.object, '__qualname__', None)
if qualname is None:
qualname = safe_getattr(self.object, '__name__', None)
if qualname and '<locals>' in qualname:
# No valid qualname found if the object is defined as locals
qualname = None

if modname == 'typing' and not self.real_modname.startswith('typing'):
return None # Python 3.6 TypeVars
elif modname and qualname:
return '.'.join([modname, qualname])

Check failure on line 1162 in sphinx/ext/autodoc/__init__.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (FLY002)

sphinx/ext/autodoc/__init__.py:1162:20: FLY002 Consider `f'{modname}.{qualname}'` instead of string join
else:
return None


class ModuleLevelDocumenter(PyObjectDocumenter):
"""
Specialized Documenter subclass for objects on module level (functions,
classes, data/constants).
Expand All @@ -1157,7 +1188,7 @@
return modname, [*parents, base]


class ClassLevelDocumenter(Documenter):
class ClassLevelDocumenter(PyObjectDocumenter):
"""
Specialized Documenter subclass for objects on class level (methods,
attributes).
Expand Down Expand Up @@ -1688,19 +1719,9 @@

return []

def get_canonical_fullname(self) -> str | None:
__modname__ = safe_getattr(self.object, '__module__', self.modname)
__qualname__ = safe_getattr(self.object, '__qualname__', None)
if __qualname__ is None:
__qualname__ = safe_getattr(self.object, '__name__', None)
if __qualname__ and '<locals>' in __qualname__:
# No valid qualname found if the object is defined as locals
__qualname__ = None

if __modname__ and __qualname__:
return f'{__modname__}.{__qualname__}'
else:
return None
def add_canonical_option(self) -> None:
if not self.doc_as_attr:
super().add_canonical_option()

def add_directive_header(self, sig: str) -> None:
sourcename = self.get_sourcename()
Expand All @@ -1715,11 +1736,6 @@
if self.analyzer and '.'.join(self.objpath) in self.analyzer.finals:
self.add_line(' :final:', sourcename)

canonical_fullname = self.get_canonical_fullname()
if (not self.doc_as_attr and not inspect.isNewType(self.object)
and canonical_fullname and self.fullname != canonical_fullname):
self.add_line(' :canonical: %s' % canonical_fullname, sourcename)

# add inheritance info, if wanted
if not self.doc_as_attr and self.options.show_inheritance:
if inspect.getorigbases(self.object):
Expand Down
2 changes: 1 addition & 1 deletion tests/roots/test-ext-autodoc/target/canonical/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
from target.canonical.original import Bar, Foo
from target.canonical.original import Bar, Foo, bar
2 changes: 2 additions & 0 deletions tests/roots/test-ext-autodoc/target/canonical/original.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ def meth(self):


def bar():
"""docstring"""

class Bar:
"""docstring"""

Expand Down
63 changes: 63 additions & 0 deletions tests/test_extensions/test_ext_autodoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -1967,12 +1967,67 @@ def test_bound_method(app):
'',
'.. py:function:: bound_method()',
' :module: target.bound_method',
' :canonical: target.bound_method.Cls.method',
'',
' Method docstring',
'',
]


@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_coroutine(app):
actual = do_autodoc(app, 'function', 'target.functions.coroutinefunc')
assert list(actual) == [
'',
'.. py:function:: coroutinefunc()',
' :module: target.functions',
' :async:',
'',
]

options = {"members": None}
actual = do_autodoc(app, 'class', 'target.coroutine.AsyncClass', options)
assert list(actual) == [
'',
'.. py:class:: AsyncClass()',
' :module: target.coroutine',
'',
'',
' .. py:method:: AsyncClass.do_coroutine()',
' :module: target.coroutine',
' :async:',
'',
' A documented coroutine function',
'',
'',
' .. py:method:: AsyncClass.do_coroutine2()',
' :module: target.coroutine',
' :async:',
' :classmethod:',
'',
' A documented coroutine classmethod',
'',
'',
' .. py:method:: AsyncClass.do_coroutine3()',
' :module: target.coroutine',
' :async:',
' :staticmethod:',
'',
' A documented coroutine staticmethod',
'',
]

# force-synchronized wrapper
actual = do_autodoc(app, 'function', 'target.coroutine.sync_func')
assert list(actual) == [
'',
'.. py:function:: sync_func()',
' :module: target.coroutine',
' :canonical: target.coroutine._other_coro_func',
'',
]


@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_partialmethod(app):
expected = [
Expand Down Expand Up @@ -2861,9 +2916,17 @@ def test_canonical(app):
'',
' .. py:method:: Foo.meth()',
' :module: target.canonical',
' :canonical: target.canonical.original.Foo.meth',
'',
' docstring',
'',
'',
'.. py:function:: bar()',
' :module: target.canonical',
' :canonical: target.canonical.original.bar',
'',
' docstring',
'',
]


Expand Down
17 changes: 17 additions & 0 deletions tests/test_extensions/test_ext_autodoc_autoattribute.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,23 @@
]


@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_autoattribute_TypeVar(app):
actual = do_autodoc(app, 'attribute', 'target.typevar.Class.T1')
assert list(actual) == [
'',
'.. py:attribute:: Class.T1',
' :module: target.typevar',
' :canonical: target.typevar.T1',
'',
' T1',
'',
" alias of TypeVar('T1')",
'',
]


@pytest.mark.skipif(sys.version_info < (3, 6), reason='python 3.6+ is required.')

Check failure on line 170 in tests/test_extensions/test_ext_autodoc_autoattribute.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (F821)

tests/test_extensions/test_ext_autodoc_autoattribute.py:170:21: F821 Undefined name `sys`
@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_autoattribute_hide_value(app):
actual = do_autodoc(app, 'attribute', 'target.hide_value.Foo.SENTINEL1')
Expand Down
5 changes: 5 additions & 0 deletions tests/test_extensions/test_ext_autodoc_autofunction.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ def test_method(app):
'',
'.. py:function:: method(arg1, arg2)',
' :module: target.callable',
' :canonical: target.callable.Callable.method',
'',
' docstring of Callable.method().',
'',
Expand All @@ -72,11 +73,14 @@ def test_method(app):

@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_builtin_function(app):
import os

actual = do_autodoc(app, 'function', 'os.umask')
assert list(actual) == [
'',
'.. py:function:: umask(mask, /)',
' :module: os',
f' :canonical: {os.name}.umask',
'',
' Set the current numeric umask and return the previous umask.',
'',
Expand All @@ -90,6 +94,7 @@ def test_methoddescriptor(app):
'',
'.. py:function:: __add__(self, value, /)',
' :module: builtins.int',
' :canonical: builtins.int.int.__add__',
'',
' Return self+value.',
'',
Expand Down
Loading