Coverage for pyquickhelper/helpgen/sphinxm_mock_app.py: 81%

189 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-03 02:21 +0200

1""" 

2@file 

3@brief Helpers to convert docstring to various format. 

4""" 

5import logging 

6import warnings 

7from docutils.parsers.rst.directives import directive as rst_directive 

8from .sphinxm_convert_doc_sphinx_helper import ( 

9 HTMLWriterWithCustomDirectives, _CustomSphinx, 

10 MDWriterWithCustomDirectives, RSTWriterWithCustomDirectives, 

11 LatexWriterWithCustomDirectives, DocTreeWriterWithCustomDirectives) 

12from ..sphinxext import get_default_extensions 

13 

14 

15class MockSphinxApp: 

16 """ 

17 Mocks :epkg:`Sphinx` application. 

18 In memory :epkg:`Sphinx` application. 

19 """ 

20 

21 def __init__(self, writer, app, confoverrides, new_extensions=None): 

22 """ 

23 @param writer see static method create 

24 @param app see static method create 

25 @param confoverrides default options 

26 @param new_extensions additional extensions 

27 """ 

28 from sphinx.registry import SphinxComponentRegistry 

29 if confoverrides is None: 

30 confoverrides = {} 

31 self.app = app 

32 self.env = app.env 

33 self.new_options = {} 

34 self.writer = writer 

35 self.registry = SphinxComponentRegistry() 

36 self.mapping = {"<class 'sphinx.ext.todo.todo_node'>": "todo", 

37 "<class 'sphinx.ext.graphviz.graphviz'>": "graphviz", 

38 "<class 'sphinx.ext.mathbase.math'>": "math", 

39 "<class 'sphinx.ext.mathbase.displaymath'>": "displaymath", 

40 "<class 'sphinx.ext.mathbase.eqref'>": "eqref"} 

41 

42 # delayed import to speed up import time 

43 from sphinx.config import Config 

44 

45 self.mapping_connect = {} 

46 with warnings.catch_warnings(): 

47 warnings.simplefilter( 

48 "ignore", (DeprecationWarning, PendingDeprecationWarning)) 

49 try: 

50 self.config = Config( # pylint: disable=E1121 

51 None, None, confoverrides, None) # pylint: disable=E1121 

52 except TypeError: 

53 # Sphinx>=3.0.0 

54 self.config = Config({}, confoverrides) 

55 self.confdir = "." 

56 self.doctreedir = "." 

57 self.srcdir = "." 

58 self.builder = writer.builder 

59 self._new_extensions = new_extensions 

60 if id(self.app) != id(self.writer.app): 

61 raise RuntimeError( # pragma: no cover 

62 "Different application in the writer is not allowed.") 

63 

64 @property 

65 def extensions(self): 

66 return self.app.extensions 

67 

68 def add_directive(self, name, cl, *args, **options): 

69 """ 

70 See :epkg:`class Sphinx`. 

71 """ 

72 # doc_directives.register_directive(name, cl) 

73 self.mapping[str(cl)] = name 

74 self.app.add_directive(name, cl, *args, **options) 

75 

76 def add_role(self, name, cl): 

77 """ 

78 See :epkg:`class Sphinx`. 

79 """ 

80 # doc_roles.register_canonical_role(name, cl) 

81 self.mapping[str(cl)] = name 

82 self.app.add_role(name, cl) 

83 

84 def add_builder(self, name, cl): 

85 """ 

86 See :epkg:`class Sphinx`. 

87 """ 

88 self.mapping[str(cl)] = name 

89 self.app.add_builder(name, cl) 

90 

91 def add_mapping(self, name, cl): 

92 """ 

93 See :epkg:`class Sphinx`. 

94 """ 

95 self.mapping[str(cl)] = name 

96 

97 def add_config_value(self, name, default, rebuild, types=()): 

98 """ 

99 See :epkg:`class Sphinx`. 

100 """ 

101 if name in self.config.values: 

102 # We do not add it a second time. 

103 return 

104 if rebuild in (False, True): 

105 rebuild = 'env' if rebuild else '' 

106 self.new_options[name] = (default, rebuild, types) 

107 self.config.values[name] = (default, rebuild, types) 

108 

109 def get_default_values(self): 

110 """ 

111 See :epkg:`class Sphinx`. 

112 """ 

113 return {k: v[0] for k, v in self.new_options.items()} 

114 

115 def add_node(self, node, **kwds): 

116 """ 

117 See :epkg:`class Sphinx`. 

118 """ 

119 self.app.add_node(node, **kwds) 

120 

121 def finalize(self, doctree, external_docnames=None): 

122 """ 

123 Finalizes the documentation after it was parsed. 

124 

125 @param doctree doctree (or pub.document), available after publication 

126 @param external_docnames other docnames the doctree references 

127 """ 

128 self.app.finalize(doctree, external_docnames=external_docnames) 

129 

130 def setup_extension(self, extname): 

131 """ 

132 See :epkg:`class Sphinx`. 

133 """ 

134 self.app.setup_extension(extname) 

135 

136 def emit(self, event, *args): 

137 """ 

138 See :epkg:`class Sphinx`. 

139 """ 

140 return self.app.events.emit(event, *args) 

141 

142 def emit_firstresult(self, event, *args): 

143 """ 

144 See :epkg:`class Sphinx`. 

145 """ 

146 return self.app.events.emit_firstresult(event, self, *args) 

147 

148 def add_autodocumenter(self, cls): 

149 """ 

150 See :epkg:`class Sphinx`. 

151 """ 

152 from sphinx.ext.autodoc.directive import AutodocDirective 

153 self.registry.add_documenter(cls.objtype, cls) 

154 self.add_directive('auto' + cls.objtype, AutodocDirective) 

155 

156 def connect(self, node, func): 

157 """ 

158 See :epkg:`class Sphinx`. 

159 """ 

160 self.mapping_connect[node] = func 

161 self.app.connect(node, func) 

162 

163 def add_domain(self, domain): 

164 """ 

165 See :epkg:`class Sphinx`. 

166 """ 

167 if domain.name in self.app.domains: 

168 # We do not register it a second time. 

169 return 

170 self.app.domains[domain.name] = domain 

171 

172 def require_sphinx(self, version): 

173 # check the Sphinx version if requested 

174 # delayed import to speed up import time 

175 from sphinx import __display_version__ as sphinx__display_version__ 

176 from sphinx.application import VersionRequirementError 

177 if version > sphinx__display_version__[:3]: 

178 raise VersionRequirementError(version) 

179 

180 def add_event(self, name): 

181 """ 

182 See :epkg:`class Sphinx`. 

183 """ 

184 if name in self.app._events: 

185 # We do not raise an exception if already present. 

186 return 

187 self.app._events[name] = '' 

188 

189 def add_env_collector(self, collector): 

190 """ 

191 See :epkg:`class Sphinx`. 

192 """ 

193 self.app.add_env_collector(collector) 

194 

195 def add_js_file(self, jsfile): 

196 """ 

197 See :epkg:`class Sphinx`. 

198 """ 

199 try: 

200 # Sphinx >= 1.8 

201 self.app.add_js_file(jsfile) 

202 except AttributeError: # pragma: no cover 

203 # Sphinx < 1.8 

204 self.app.add_javascript(jsfile) 

205 

206 def add_css_file(self, css): 

207 """ 

208 See :epkg:`class Sphinx`. 

209 """ 

210 try: 

211 # Sphinx >= 1.8 

212 self.app.add_css_file(css) 

213 except AttributeError: # pragma: no cover 

214 # Sphinx < 1.8 

215 self.app.add_stylesheet(css) 

216 

217 def add_source_parser(self, ext, parser, exc=False): 

218 """ 

219 Registers a parser for a specific file extension. 

220 

221 @param ext file extension 

222 @param parser parser 

223 @param exc raises an exception if already done 

224 

225 Example: 

226 

227 :: 

228 

229 app.add_source_parser(self, ext, parser) 

230 """ 

231 # delayed import to speed up import time 

232 from sphinx.errors import ExtensionError 

233 with warnings.catch_warnings(): 

234 warnings.simplefilter("ignore", ImportWarning) 

235 

236 try: 

237 self.app.add_source_parser(ext, parser) 

238 except TypeError: # pragma: no cover 

239 # Sphinx==3.0.0 

240 self.app.add_source_parser(parser) 

241 except ExtensionError as e: # pragma: no cover 

242 if exc: 

243 raise 

244 logger = logging.getLogger("MockSphinxApp") 

245 logger.warning('[MockSphinxApp] %s', e) 

246 

247 def disconnect_env_collector(self, clname): 

248 """ 

249 Disconnects a collector given its class name. 

250 

251 @param cl name 

252 @return found collector 

253 """ 

254 self.app.disconnect_env_collector(clname) 

255 

256 @staticmethod 

257 def create(writer="html", directives=None, confoverrides=None, 

258 new_extensions=None, destination_path=None, fLOG=None): 

259 """ 

260 Creates a @see cl MockSphinxApp for :epkg:`Sphinx`. 

261 

262 @param writer ``'sphinx'`` is the only allowed value 

263 @param directives new directives to add (see below) 

264 @param confoverrides initial options 

265 @param new_extensions additional extensions to setup 

266 @param destination_path some extension requires it 

267 @param fLOG logging function 

268 @return mockapp, writer, list of added nodes 

269 

270 *directives* is None or a list of 2 or 5-uple: 

271 

272 * a directive name (mandatory) 

273 * a directive class: see `Sphinx Directive 

274 <https://www.sphinx-doc.org/en/master/development/tutorials/helloworld.html>`_, 

275 see also @see cl RunPythonDirective as an example (mandatory) 

276 * a docutils node: see @see cl runpython_node as an example 

277 * two functions: see @see fn visit_runpython_node, 

278 @see fn depart_runpython_node as an example 

279 """ 

280 logger = logging.getLogger('gdot') 

281 if not logger.disabled: 

282 logger.disabled = True 

283 restore = True 

284 else: 

285 restore = False 

286 

287 with warnings.catch_warnings(): 

288 warnings.simplefilter( 

289 "ignore", (DeprecationWarning, PendingDeprecationWarning)) 

290 if confoverrides is None: 

291 confoverrides = {} 

292 if "extensions" not in confoverrides: 

293 confoverrides["extensions"] = get_default_extensions() 

294 

295 if writer in ("sphinx", "custom", "HTMLWriterWithCustomDirectives", "html"): 

296 app = _CustomSphinx(srcdir=None, confdir=None, outdir=destination_path, 

297 doctreedir=None, buildername='memoryhtml', 

298 confoverrides=confoverrides, new_extensions=new_extensions) 

299 writer = HTMLWriterWithCustomDirectives( 

300 builder=app.builder, app=app) 

301 mockapp = MockSphinxApp(writer, writer.app, confoverrides=confoverrides, 

302 new_extensions=new_extensions) 

303 elif writer == "rst": 

304 app = _CustomSphinx(srcdir=None, confdir=None, outdir=destination_path, 

305 doctreedir=None, 

306 buildername='memoryrst', confoverrides=confoverrides, 

307 new_extensions=new_extensions) 

308 writer = RSTWriterWithCustomDirectives( 

309 builder=app.builder, app=app) 

310 mockapp = MockSphinxApp(writer, writer.app, confoverrides=confoverrides, 

311 new_extensions=new_extensions) 

312 elif writer == "md": 

313 app = _CustomSphinx(srcdir=None, confdir=None, outdir=destination_path, 

314 doctreedir=None, 

315 buildername='memorymd', confoverrides=confoverrides, 

316 new_extensions=new_extensions) 

317 writer = MDWriterWithCustomDirectives( 

318 builder=app.builder, app=app) 

319 mockapp = MockSphinxApp(writer, writer.app, confoverrides=confoverrides, 

320 new_extensions=new_extensions) 

321 elif writer == "elatex": 

322 app = _CustomSphinx(srcdir=None, confdir=None, outdir=None, doctreedir=None, 

323 buildername='memorylatex', confoverrides=confoverrides, 

324 new_extensions=new_extensions) 

325 writer = LatexWriterWithCustomDirectives( 

326 builder=app.builder, app=app) 

327 mockapp = MockSphinxApp(writer, writer.app, confoverrides=confoverrides, 

328 new_extensions=new_extensions) 

329 elif writer == "doctree": 

330 app = _CustomSphinx(srcdir=None, confdir=None, outdir=destination_path, 

331 doctreedir=None, 

332 buildername='memorydoctree', confoverrides=confoverrides, 

333 new_extensions=new_extensions) 

334 writer = DocTreeWriterWithCustomDirectives( 

335 builder=app.builder, app=app) 

336 mockapp = MockSphinxApp(writer, writer.app, confoverrides=confoverrides, 

337 new_extensions=new_extensions) 

338 elif isinstance(writer, tuple): 

339 # We expect ("builder_name", builder_class) 

340 app = _CustomSphinx(srcdir=None, confdir=None, outdir=destination_path, 

341 doctreedir=None, 

342 buildername=writer, confoverrides=confoverrides, 

343 new_extensions=new_extensions) 

344 if not hasattr(writer[1], "_writer_class"): 

345 raise AttributeError( # pragma: no cover 

346 "Class '{0}' does not have any attribute '_writer_class'." 

347 "".format(writer[1])) 

348 writer = writer[1]._writer_class( # pylint: disable=E1101 

349 builder=app.builder, app=app) # pylint: disable=E1101 

350 mockapp = MockSphinxApp(writer, app, confoverrides=confoverrides, 

351 new_extensions=new_extensions) 

352 else: 

353 raise ValueError( 

354 f"Writer must be 'html', 'rst', 'md', 'elatex', not '{writer}'.") 

355 

356 if restore: 

357 logger.disabled = False 

358 

359 # titles 

360 title_names = [] 

361 title_names.append("todoext_node") 

362 title_names.append("todo_node") 

363 title_names.append("mathdef_node") 

364 title_names.append("blocref_node") 

365 title_names.append("faqref_node") 

366 title_names.append("nbref_node") 

367 title_names.append("exref_node") 

368 

369 if directives is not None: 

370 for tu in directives: 

371 if len(tu) < 2: 

372 raise ValueError( 

373 "directives is a list of tuple with at least two elements, check the documentation") 

374 if len(tu) == 5: 

375 name, cl, node, f1, f2 = tu 

376 mockapp.add_node(node, html=(f1, f2)) 

377 # not necessary 

378 # nodes._add_node_class_names([node.__name__]) 

379 writer.connect_directive_node(node.__name__, f1, f2) 

380 elif len(tu) != 2: 

381 raise ValueError( 

382 "directives is a list of tuple with 2 or 5 elements, check the documentation") 

383 name, cl = tu[:2] 

384 mockapp.add_directive(name, cl) 

385 

386 if fLOG: 

387 apps = [mockapp] 

388 if hasattr(writer, "app"): 

389 apps.append(writer.app) 

390 for app in apps: 

391 if hasattr(app, "_added_objects"): 

392 fLOG("[MockSphinxApp] list of added objects") 

393 for el in app._added_objects: 

394 fLOG("[MockSphinxApp]", el) 

395 if el[0] == "domain": 

396 fLOG("[MockSphinxApp] NAME", el[1].name) 

397 for ro in el[1].roles: 

398 fLOG("[MockSphinxApp] ROLES", ro) 

399 for ro in el[1].directives: 

400 fLOG("[MockSphinxApp] DIREC", ro) 

401 from docutils.parsers.rst.directives import _directives 

402 for res in sorted(_directives): 

403 fLOG("[MockSphinxApp] RST DIREC", res) 

404 

405 class bb: 

406 def info(*args, line=0): # pylint: disable=E0211,E0213 

407 fLOG("[MockSphinxApp] -- ", *args) 

408 

409 class aa: 

410 def __init__(self): 

411 self.reporter = bb() 

412 self.current_line = 0 

413 from docutils.parsers.rst.languages import en 

414 for dir_check in ['py:function']: 

415 res = rst_directive(dir_check, en, aa()) 

416 

417 return mockapp, writer, title_names