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

1""" 

2============= 

3autointerface 

4============= 

5 

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. 

10 

11.. rst:directive:: autointerface 

12 

13 The :rst:dir:`autointerface` directive has the same form and option as the 

14 :rst:dir:`sphinx:autoclass` directive:: 

15 

16 .. autointerface:: IClass 

17 ... 

18 

19 .. seealso:: :mod:`sphinx.ext.autodoc` 

20 

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": 

24 

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. 

29 

30====================== 

31Implementation Details 

32====================== 

33 

34.. autosummary:: 

35 

36 interface_getattr 

37 interface_format_args 

38 InterfaceDocumenter 

39 InterfaceAttributeDocumenter 

40 InterfaceMethodDocumenter 

41 InterfaceDirective 

42 setup 

43 

44""" 

45from typing import Any, Dict, Tuple 

46 

47import sphinx.ext.autodoc 

48import sphinx.domains.python 

49import sphinx.roles 

50from sphinx.locale import _, __ 

51from sphinx.application import Sphinx 

52 

53import zope.interface.interface 

54 

55from sphinx.ext.autodoc import ( 

56 ClassDocumenter, 

57 ObjectMembers, 

58 logger, 

59) 

60from sphinx.domains.python import PyXRefRole 

61 

62from . import __version__ 

63 

64 

65def interface_getattr(*v): 

66 """Behaves like `getattr` but for zope Interface objects which 

67 hide the attributes. 

68 

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()) 

80 

81 if name in obj.names(all=True): 

82 return obj.get(name) 

83 else: 

84 return getattr(*v) 

85 

86 

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 

97 

98 

99class InterfaceDocumenter(ClassDocumenter): 

100 """A Documenter for :class:`zope.interface.Interface` interfaces.""" 

101 

102 objtype = "interface" 

103 directivetype = "interface" 

104 

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 

108 

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) 

114 

115 def format_args(self) -> str: 

116 return interface_format_args(self.object) 

117 

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*. 

122 

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 ] 

151 

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 

157 

158 constructor = obj.get("__init__") 

159 if not constructor: 

160 return 

161 

162 # A bit of a hack, but works properly. 

163 obj.__init__ = constructor 

164 return 

165 

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) 

170 

171 if show_inheritance: 

172 self.options.show_inheritance = True 

173 

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) 

182 

183 

184class InterfaceAttributeDocumenter(sphinx.ext.autodoc.AttributeDocumenter): 

185 """A Documenter for :class:`zope.interface.interface.Attribute` 

186 interface attributes. 

187 """ 

188 

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' 

193 

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 

200 

201 def isslotsattribute(self) -> bool: 

202 return False 

203 

204 def generate(self, *v, **kw): 

205 super().generate(*v, **kw) 

206 

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) 

211 

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) 

219 

220 

221class InterfaceMethodDocumenter(sphinx.ext.autodoc.MethodDocumenter): 

222 """ 

223 A Documenter for :class:`zope.interface.interface.Method` 

224 interface attributes. 

225 """ 

226 

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' 

231 

232 @classmethod 

233 def can_document_member(cls, member, membername, isattr, parent): 

234 return isinstance(member, zope.interface.interface.Method) 

235 

236 def format_args(self): 

237 return interface_format_args(self.object) 

238 

239 

240class InterfaceDirective(sphinx.domains.python.PyClasslike): 

241 r"""An `'interface'` directive.""" 

242 

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 "" 

248 

249 

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""" 

258 

259 

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) 

268 

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 ) 

274 

275 app.add_js_file(None, body=_JS_TO_ADD_CLASS_TO_INTERFACE) 

276 return {"version": __version__}