Coverage for /Users/mforbes/work/mmfgh/zopeext/.venv/lib/python3.10/site-packages/sphinxcontrib/zopeext/autointerface.py: 95%
117 statements
« prev ^ index » next coverage.py v7.2.3, created at 2023-04-16 23:40 -0700
« prev ^ index » next coverage.py v7.2.3, created at 2023-04-16 23:40 -0700
1"""
2=============
3autointerface
4=============
6This Sphinx extension adds an :rst:dir:`autointerface` directive, which can be
7used like :rst:dir:`sphinx:autoclass` to document zope interfaces. Interfaces
8are intended to be very different beasts than regular python classes, and as a
9result require customized access to documentation, signatures etc.
11.. rst:directive:: autointerface
13 The :rst:dir:`autointerface` directive has the same form and option as the
14 :rst:dir:`sphinx:autoclass` directive::
16 .. autointerface:: IClass
17 ...
19 .. seealso:: :mod:`sphinx.ext.autodoc`
21 .. note:: This extension also serves as a simple example of using the sphinx
22 version 0.6 :mod:`sphinx.ext.autodoc` refactoring. Mostly this was
23 straight forward, but I stumbled across one "gotcha":
25 The `objtype` attribute of the documenters needs to be unique. Thus, for
26 example, :attr:`InterfaceMethodDocumenter.objtype` cannot be `'method'`
27 because this would overwrite the entry in :attr:`AutoDirective._registry`
28 used to choose the correct documenter.
30======================
31Implementation Details
32======================
34.. autosummary::
36 interface_getattr
37 interface_format_args
38 InterfaceDocumenter
39 InterfaceAttributeDocumenter
40 InterfaceMethodDocumenter
41 InterfaceDirective
42 setup
44"""
45from typing import Any, Dict, Tuple
47import sphinx.ext.autodoc
48import sphinx.domains.python
49import sphinx.roles
50from sphinx.locale import _, __
51from sphinx.application import Sphinx
53import zope.interface.interface
55from sphinx.ext.autodoc import (
56 ClassDocumenter,
57 ObjectMembers,
58 logger,
59)
60from sphinx.domains.python import PyXRefRole
62from . import __version__
65def interface_getattr(*v):
66 """Behaves like `getattr` but for zope Interface objects which
67 hide the attributes.
69 .. note:: Originally I simply tried to
70 override :meth:`InterfaceDocumenter.special_attrgetter` to deal with the
71 special access needs of :class:`Interface` objects, but found that this
72 is not intended to be overwritten. Instead one should register the
73 special accessor using :func:`app.add_autodoc_attrgetter`.
74 """
75 obj, name = v[:2]
76 if "__dict__" == name:
77 # Interface objects do not list their members through
78 # __dict__.
79 return dict((n, obj.get(n)) for n in obj.names())
81 if name in obj.names(all=True):
82 return obj.get(name)
83 else:
84 return getattr(*v)
87def interface_format_args(obj):
88 """Return the signature of an interface method or of an
89 interface."""
90 sig = "()"
91 if isinstance(obj, zope.interface.interface.InterfaceClass):
92 if "__init__" in obj:
93 sig = interface_format_args(obj.get("__init__"))
94 else:
95 sig = obj.getSignatureString()
96 return sig
99class InterfaceDocumenter(ClassDocumenter):
100 """A Documenter for :class:`zope.interface.Interface` interfaces."""
102 objtype = "interface"
103 directivetype = "interface"
105 # Since these have very specific tests, we give the classes defined here
106 # very high priority so that they override any other documenters.
107 priority = 100 + ClassDocumenter.priority
109 @classmethod
110 def can_document_member(
111 cls, member: Any, membername: str, isattr: bool, parent: Any
112 ) -> bool:
113 return isinstance(member, zope.interface.interface.InterfaceClass)
115 def format_args(self) -> str:
116 return interface_format_args(self.object)
118 def get_object_members(self, want_all: bool) -> Tuple[bool, ObjectMembers]:
119 """
120 Return `(members_check_module, members)` where `members` is a
121 list of `(membername, member)` pairs of the members of *self.object*.
123 If *want_all* is True, return all members. Else, only return those
124 members given by *self.options.members* (which may also be None).
125 """
126 obj = self.object
127 names = sorted(obj.names(all=want_all))
128 if not want_all:
129 if not self.options.members:
130 return False, [] # type: ignore
131 # specific members given
132 selected = []
133 for name in self.options.members: # type: str
134 if name in names:
135 selected.append((name, obj.get(name)))
136 else:
137 logger.warning(
138 __("missing attribute %s in interface %s")
139 % (name, self.fullname),
140 type="autointerface",
141 )
142 return False, selected
143 elif self.options.inherited_members:
144 return False, [(_name, obj.get(_name)) for _name in names]
145 else:
146 return False, [
147 (_name, obj.get(_name))
148 for _name in names
149 if obj.get(_name).interface == self.object
150 ]
152 @staticmethod
153 def autodoc_process_docstring(app, what, name, obj, options, lines):
154 """Hook that adds the constructor to the object so it can be found."""
155 if not isinstance(obj, zope.interface.interface.InterfaceClass):
156 return
158 constructor = obj.get("__init__")
159 if not constructor:
160 return
162 # A bit of a hack, but works properly.
163 obj.__init__ = constructor
164 return
166 def add_directive_header(self, sig: str) -> None:
167 show_inheritance = self.options.show_inheritance
168 self.options.show_inheritance = False
169 super().add_directive_header(sig)
171 if show_inheritance:
172 self.options.show_inheritance = True
174 # add inheritance info, if wanted
175 if not self.doc_as_attr and self.options.show_inheritance:
176 sourcename = self.get_sourcename()
177 self.add_line("", sourcename)
178 bases_ = self.object.getBases()
179 if bases_:
180 bases = [":class:`%s.%s`" % (b.__module__, b.__name__) for b in bases_]
181 self.add_line(" " + _("Bases: %s") % ", ".join(bases), sourcename)
184class InterfaceAttributeDocumenter(sphinx.ext.autodoc.AttributeDocumenter):
185 """A Documenter for :class:`zope.interface.interface.Attribute`
186 interface attributes.
187 """
189 objtype = "interfaceattribute" # Called 'autointerfaceattribute'
190 directivetype = "attribute" # Formats as a 'attribute' for now
191 priority = 100 + sphinx.ext.autodoc.AttributeDocumenter.priority
192 member_order = 60 # Order when 'groupwise'
194 @classmethod
195 def can_document_member(cls, member, membername, isattr, parent):
196 res = isinstance(member, zope.interface.interface.Attribute) and not isinstance(
197 member, zope.interface.interface.Method
198 )
199 return res
201 def isslotsattribute(self) -> bool:
202 return False
204 def generate(self, *v, **kw):
205 super().generate(*v, **kw)
207 def add_directive_header(self, sig: str) -> None:
208 # Hack to remove the value.
209 self.non_data_descriptor = False
210 super().add_directive_header(sig)
212 def add_content(self, more_content):
213 # Correct behavior of AttributeDocumenter.add_content.
214 # Don't run the source analyzer... just get the documentation
215 self.analyzer = None
216 # Treat attributes as datadescriptors since they have docstrings
217 self.non_data_descriptor = False
218 super().add_content(more_content)
221class InterfaceMethodDocumenter(sphinx.ext.autodoc.MethodDocumenter):
222 """
223 A Documenter for :class:`zope.interface.interface.Method`
224 interface attributes.
225 """
227 objtype = "interfacemethod" # Called 'autointerfacemethod'
228 directivetype = "method" # Formats as a 'method' for now
229 priority = 100 + sphinx.ext.autodoc.MethodDocumenter.priority
230 member_order = 70 # Order when 'groupwise'
232 @classmethod
233 def can_document_member(cls, member, membername, isattr, parent):
234 return isinstance(member, zope.interface.interface.Method)
236 def format_args(self):
237 return interface_format_args(self.object)
240class InterfaceDirective(sphinx.domains.python.PyClasslike):
241 r"""An `'interface'` directive."""
243 def get_index_text(self, modname, name_cls):
244 if self.objtype == "interface":
245 return _("%s (interface in %s)") % (name_cls[0], modname)
246 else:
247 return ""
250# Many themes provide no styling for interfaces, so we add some javascript here that
251# inserts "class" as well, so that interfaces fallback to class formatting. (I.e. the
252# HTML `class="py interface"` will become `class="py interface class"`
253_JS_TO_ADD_CLASS_TO_INTERFACE = """
254$(document).ready(function() {
255 $('.interface').addClass('class');
256});
257"""
260def setup(app: Sphinx) -> Dict[str, Any]:
261 app.setup_extension("sphinx.ext.autodoc")
262 app.add_autodoc_attrgetter(
263 zope.interface.interface.InterfaceClass, interface_getattr
264 )
265 app.add_autodocumenter(InterfaceDocumenter)
266 app.add_autodocumenter(InterfaceAttributeDocumenter)
267 app.add_autodocumenter(InterfaceMethodDocumenter)
269 app.add_directive_to_domain("py", "interface", InterfaceDirective)
270 app.add_role_to_domain("py", "interface", PyXRefRole())
271 app.connect(
272 "autodoc-process-docstring", InterfaceDocumenter.autodoc_process_docstring
273 )
275 app.add_js_file(None, body=_JS_TO_ADD_CLASS_TO_INTERFACE)
276 return {"version": __version__}