PageRenderTime 58ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 1ms

/External.LCA_RESTRICTED/Languages/IronPython/27/Doc/sphinx/environment.py

http://github.com/IronLanguages/main
Python | 1671 lines | 1555 code | 44 blank | 72 comment | 83 complexity | c2e2901affa1e7a059d4a246c5744338 MD5 | raw file
Possible License(s): CPL-1.0, BSD-3-Clause, ISC, GPL-2.0, MPL-2.0-no-copyleft-exception
  1. # -*- coding: utf-8 -*-
  2. """
  3. sphinx.environment
  4. ~~~~~~~~~~~~~~~~~~
  5. Global creation environment.
  6. :copyright: Copyright 2007-2009 by the Sphinx team, see AUTHORS.
  7. :license: BSD, see LICENSE for details.
  8. """
  9. import re
  10. import os
  11. import time
  12. import heapq
  13. import types
  14. import codecs
  15. import imghdr
  16. import string
  17. import difflib
  18. import cPickle as pickle
  19. from os import path
  20. from glob import glob
  21. from itertools import izip, groupby
  22. from docutils import nodes
  23. from docutils.io import FileInput, NullOutput
  24. from docutils.core import Publisher
  25. from docutils.utils import Reporter, relative_path
  26. from docutils.readers import standalone
  27. from docutils.parsers.rst import roles
  28. from docutils.parsers.rst.languages import en as english
  29. from docutils.parsers.rst.directives.html import MetaBody
  30. from docutils.writers import UnfilteredWriter
  31. from docutils.transforms import Transform
  32. from docutils.transforms.parts import ContentsFilter
  33. from sphinx import addnodes
  34. from sphinx.util import movefile, get_matching_docs, SEP, ustrftime, \
  35. docname_join, FilenameUniqDict, url_re
  36. from sphinx.errors import SphinxError
  37. from sphinx.directives import additional_xref_types
  38. default_settings = {
  39. 'embed_stylesheet': False,
  40. 'cloak_email_addresses': True,
  41. 'pep_base_url': 'http://www.python.org/dev/peps/',
  42. 'rfc_base_url': 'http://tools.ietf.org/html/',
  43. 'input_encoding': 'utf-8-sig',
  44. 'doctitle_xform': False,
  45. 'sectsubtitle_xform': False,
  46. }
  47. # This is increased every time an environment attribute is added
  48. # or changed to properly invalidate pickle files.
  49. ENV_VERSION = 30
  50. default_substitutions = set([
  51. 'version',
  52. 'release',
  53. 'today',
  54. ])
  55. dummy_reporter = Reporter('', 4, 4)
  56. class WarningStream(object):
  57. def __init__(self, warnfunc):
  58. self.warnfunc = warnfunc
  59. def write(self, text):
  60. if text.strip():
  61. self.warnfunc(text, None, '')
  62. class NoUri(Exception):
  63. """Raised by get_relative_uri if there is no URI available."""
  64. pass
  65. class DefaultSubstitutions(Transform):
  66. """
  67. Replace some substitutions if they aren't defined in the document.
  68. """
  69. # run before the default Substitutions
  70. default_priority = 210
  71. def apply(self):
  72. config = self.document.settings.env.config
  73. # only handle those not otherwise defined in the document
  74. to_handle = default_substitutions - set(self.document.substitution_defs)
  75. for ref in self.document.traverse(nodes.substitution_reference):
  76. refname = ref['refname']
  77. if refname in to_handle:
  78. text = config[refname]
  79. if refname == 'today' and not text:
  80. # special handling: can also specify a strftime format
  81. text = ustrftime(config.today_fmt or _('%B %d, %Y'))
  82. ref.replace_self(nodes.Text(text, text))
  83. class MoveModuleTargets(Transform):
  84. """
  85. Move module targets to their nearest enclosing section title.
  86. """
  87. default_priority = 210
  88. def apply(self):
  89. for node in self.document.traverse(nodes.target):
  90. if not node['ids']:
  91. continue
  92. if node['ids'][0].startswith('module-') and \
  93. node.parent.__class__ is nodes.section and \
  94. node.has_key('ismod'):
  95. node.parent['ids'] = node['ids']
  96. node.parent.remove(node)
  97. class HandleCodeBlocks(Transform):
  98. """
  99. Move doctest blocks out of blockquotes.
  100. """
  101. default_priority = 210
  102. def apply(self):
  103. for node in self.document.traverse(nodes.block_quote):
  104. if len(node.children) == 1 and isinstance(node.children[0],
  105. nodes.doctest_block):
  106. node.replace_self(node.children[0])
  107. class SortIds(Transform):
  108. """
  109. Sort secion IDs so that the "id[0-9]+" one comes last.
  110. """
  111. default_priority = 261
  112. def apply(self):
  113. for node in self.document.traverse(nodes.section):
  114. if len(node['ids']) > 1 and node['ids'][0].startswith('id'):
  115. node['ids'] = node['ids'][1:] + [node['ids'][0]]
  116. class CitationReferences(Transform):
  117. """
  118. Handle citation references before the default docutils transform does.
  119. """
  120. default_priority = 619
  121. def apply(self):
  122. for citnode in self.document.traverse(nodes.citation_reference):
  123. cittext = citnode.astext()
  124. refnode = addnodes.pending_xref(cittext, reftype='citation',
  125. reftarget=cittext)
  126. refnode += nodes.Text('[' + cittext + ']')
  127. citnode.parent.replace(citnode, refnode)
  128. class SphinxStandaloneReader(standalone.Reader):
  129. """
  130. Add our own transforms.
  131. """
  132. transforms = [CitationReferences, DefaultSubstitutions, MoveModuleTargets,
  133. HandleCodeBlocks, SortIds]
  134. def get_transforms(self):
  135. return standalone.Reader.get_transforms(self) + self.transforms
  136. class SphinxDummyWriter(UnfilteredWriter):
  137. supported = ('html',) # needed to keep "meta" nodes
  138. def translate(self):
  139. pass
  140. class SphinxContentsFilter(ContentsFilter):
  141. """
  142. Used with BuildEnvironment.add_toc_from() to discard cross-file links
  143. within table-of-contents link nodes.
  144. """
  145. def visit_pending_xref(self, node):
  146. text = node.astext()
  147. self.parent.append(nodes.literal(text, text))
  148. raise nodes.SkipNode
  149. def visit_image(self, node):
  150. raise nodes.SkipNode
  151. class BuildEnvironment:
  152. """
  153. The environment in which the ReST files are translated.
  154. Stores an inventory of cross-file targets and provides doctree
  155. transformations to resolve links to them.
  156. """
  157. # --------- ENVIRONMENT PERSISTENCE ----------------------------------------
  158. @staticmethod
  159. def frompickle(config, filename):
  160. picklefile = open(filename, 'rb')
  161. try:
  162. env = pickle.load(picklefile)
  163. finally:
  164. picklefile.close()
  165. env.config.values = config.values
  166. if env.version != ENV_VERSION:
  167. raise IOError('env version not current')
  168. return env
  169. def topickle(self, filename):
  170. # remove unpicklable attributes
  171. warnfunc = self._warnfunc
  172. self.set_warnfunc(None)
  173. values = self.config.values
  174. del self.config.values
  175. # first write to a temporary file, so that if dumping fails,
  176. # the existing environment won't be overwritten
  177. picklefile = open(filename + '.tmp', 'wb')
  178. # remove potentially pickling-problematic values from config
  179. for key, val in vars(self.config).items():
  180. if key.startswith('_') or \
  181. isinstance(val, types.ModuleType) or \
  182. isinstance(val, types.FunctionType) or \
  183. isinstance(val, (type, types.ClassType)):
  184. del self.config[key]
  185. try:
  186. pickle.dump(self, picklefile, pickle.HIGHEST_PROTOCOL)
  187. finally:
  188. picklefile.close()
  189. movefile(filename + '.tmp', filename)
  190. # reset attributes
  191. self.config.values = values
  192. self.set_warnfunc(warnfunc)
  193. # --------- ENVIRONMENT INITIALIZATION -------------------------------------
  194. def __init__(self, srcdir, doctreedir, config):
  195. self.doctreedir = doctreedir
  196. self.srcdir = srcdir
  197. self.config = config
  198. # the application object; only set while update() runs
  199. self.app = None
  200. # the docutils settings for building
  201. self.settings = default_settings.copy()
  202. self.settings['env'] = self
  203. # the function to write warning messages with
  204. self._warnfunc = None
  205. # this is to invalidate old pickles
  206. self.version = ENV_VERSION
  207. # All "docnames" here are /-separated and relative and exclude
  208. # the source suffix.
  209. self.found_docs = set() # contains all existing docnames
  210. self.all_docs = {} # docname -> mtime at the time of build
  211. # contains all built docnames
  212. self.dependencies = {} # docname -> set of dependent file
  213. # names, relative to documentation root
  214. # File metadata
  215. self.metadata = {} # docname -> dict of metadata items
  216. # TOC inventory
  217. self.titles = {} # docname -> title node
  218. self.longtitles = {} # docname -> title node; only different if
  219. # set differently with title directive
  220. self.tocs = {} # docname -> table of contents nodetree
  221. self.toc_num_entries = {} # docname -> number of real entries
  222. # used to determine when to show the TOC
  223. # in a sidebar (don't show if it's only one item)
  224. self.toc_secnumbers = {} # docname -> dict of sectionid -> number
  225. self.toctree_includes = {} # docname -> list of toctree includefiles
  226. self.files_to_rebuild = {} # docname -> set of files
  227. # (containing its TOCs) to rebuild too
  228. self.glob_toctrees = set() # docnames that have :glob: toctrees
  229. self.numbered_toctrees = set() # docnames that have :numbered: toctrees
  230. # X-ref target inventory
  231. self.descrefs = {} # fullname -> docname, desctype
  232. self.filemodules = {} # docname -> [modules]
  233. self.modules = {} # modname -> docname, synopsis,
  234. # platform, deprecated
  235. self.labels = {} # labelname -> docname, labelid, sectionname
  236. self.anonlabels = {} # labelname -> docname, labelid
  237. self.progoptions = {} # (program, name) -> docname, labelid
  238. self.reftargets = {} # (type, name) -> docname, labelid
  239. # type: term, token, envvar, citation
  240. # Other inventories
  241. self.indexentries = {} # docname -> list of
  242. # (type, string, target, aliasname)
  243. self.versionchanges = {} # version -> list of (type, docname,
  244. # lineno, module, descname, content)
  245. # these map absolute path -> (docnames, unique filename)
  246. self.images = FilenameUniqDict()
  247. self.dlfiles = FilenameUniqDict()
  248. # These are set while parsing a file
  249. self.docname = None # current document name
  250. self.currmodule = None # current module name
  251. self.currclass = None # current class name
  252. self.currdesc = None # current descref name
  253. self.currprogram = None # current program name
  254. self.index_num = 0 # autonumber for index targets
  255. self.gloss_entries = set() # existing definition labels
  256. # Some magically present labels
  257. def add_magic_label(name, description):
  258. self.labels[name] = (name, '', description)
  259. self.anonlabels[name] = (name, '')
  260. add_magic_label('genindex', _('Index'))
  261. add_magic_label('modindex', _('Module Index'))
  262. add_magic_label('search', _('Search Page'))
  263. def set_warnfunc(self, func):
  264. self._warnfunc = func
  265. self.settings['warning_stream'] = WarningStream(func)
  266. def warn(self, docname, msg, lineno=None):
  267. if docname:
  268. if lineno is None:
  269. lineno = ''
  270. self._warnfunc(msg, '%s:%s' % (self.doc2path(docname), lineno))
  271. else:
  272. self._warnfunc(msg)
  273. def clear_doc(self, docname):
  274. """Remove all traces of a source file in the inventory."""
  275. if docname in self.all_docs:
  276. self.all_docs.pop(docname, None)
  277. self.metadata.pop(docname, None)
  278. self.dependencies.pop(docname, None)
  279. self.titles.pop(docname, None)
  280. self.longtitles.pop(docname, None)
  281. self.tocs.pop(docname, None)
  282. self.toc_secnumbers.pop(docname, None)
  283. self.toc_num_entries.pop(docname, None)
  284. self.toctree_includes.pop(docname, None)
  285. self.filemodules.pop(docname, None)
  286. self.indexentries.pop(docname, None)
  287. self.glob_toctrees.discard(docname)
  288. self.numbered_toctrees.discard(docname)
  289. self.images.purge_doc(docname)
  290. self.dlfiles.purge_doc(docname)
  291. for subfn, fnset in self.files_to_rebuild.items():
  292. fnset.discard(docname)
  293. if not fnset:
  294. del self.files_to_rebuild[subfn]
  295. for fullname, (fn, _) in self.descrefs.items():
  296. if fn == docname:
  297. del self.descrefs[fullname]
  298. for modname, (fn, _, _, _) in self.modules.items():
  299. if fn == docname:
  300. del self.modules[modname]
  301. for labelname, (fn, _, _) in self.labels.items():
  302. if fn == docname:
  303. del self.labels[labelname]
  304. for key, (fn, _) in self.reftargets.items():
  305. if fn == docname:
  306. del self.reftargets[key]
  307. for key, (fn, _) in self.progoptions.items():
  308. if fn == docname:
  309. del self.progoptions[key]
  310. for version, changes in self.versionchanges.items():
  311. new = [change for change in changes if change[1] != docname]
  312. changes[:] = new
  313. def doc2path(self, docname, base=True, suffix=None):
  314. """
  315. Return the filename for the document name.
  316. If base is True, return absolute path under self.srcdir.
  317. If base is None, return relative path to self.srcdir.
  318. If base is a path string, return absolute path under that.
  319. If suffix is not None, add it instead of config.source_suffix.
  320. """
  321. suffix = suffix or self.config.source_suffix
  322. if base is True:
  323. return path.join(self.srcdir,
  324. docname.replace(SEP, path.sep)) + suffix
  325. elif base is None:
  326. return docname.replace(SEP, path.sep) + suffix
  327. else:
  328. return path.join(base, docname.replace(SEP, path.sep)) + suffix
  329. def find_files(self, config):
  330. """
  331. Find all source files in the source dir and put them in self.found_docs.
  332. """
  333. exclude_dirs = [d.replace(SEP, path.sep) for d in config.exclude_dirs]
  334. exclude_trees = [d.replace(SEP, path.sep) for d in config.exclude_trees]
  335. self.found_docs = set(get_matching_docs(
  336. self.srcdir, config.source_suffix,
  337. exclude_docs=set(config.unused_docs),
  338. exclude_dirs=exclude_dirs,
  339. exclude_trees=exclude_trees,
  340. exclude_dirnames=['_sources'] + config.exclude_dirnames))
  341. def get_outdated_files(self, config_changed):
  342. """
  343. Return (added, changed, removed) sets.
  344. """
  345. # clear all files no longer present
  346. removed = set(self.all_docs) - self.found_docs
  347. added = set()
  348. changed = set()
  349. if config_changed:
  350. # config values affect e.g. substitutions
  351. added = self.found_docs
  352. else:
  353. for docname in self.found_docs:
  354. if docname not in self.all_docs:
  355. added.add(docname)
  356. continue
  357. # if the doctree file is not there, rebuild
  358. if not path.isfile(self.doc2path(docname, self.doctreedir,
  359. '.doctree')):
  360. changed.add(docname)
  361. continue
  362. # check the mtime of the document
  363. mtime = self.all_docs[docname]
  364. newmtime = path.getmtime(self.doc2path(docname))
  365. if newmtime > mtime:
  366. changed.add(docname)
  367. continue
  368. # finally, check the mtime of dependencies
  369. for dep in self.dependencies.get(docname, ()):
  370. try:
  371. # this will do the right thing when dep is absolute too
  372. deppath = path.join(self.srcdir, dep)
  373. if not path.isfile(deppath):
  374. changed.add(docname)
  375. break
  376. depmtime = path.getmtime(deppath)
  377. if depmtime > mtime:
  378. changed.add(docname)
  379. break
  380. except EnvironmentError:
  381. # give it another chance
  382. changed.add(docname)
  383. break
  384. return added, changed, removed
  385. def update(self, config, srcdir, doctreedir, app=None):
  386. """
  387. (Re-)read all files new or changed since last update. Returns a
  388. summary, the total count of documents to reread and an iterator that
  389. yields docnames as it processes them. Store all environment docnames in
  390. the canonical format (ie using SEP as a separator in place of
  391. os.path.sep).
  392. """
  393. config_changed = False
  394. if self.config is None:
  395. msg = '[new config] '
  396. config_changed = True
  397. else:
  398. # check if a config value was changed that affects how
  399. # doctrees are read
  400. for key, descr in config.values.iteritems():
  401. if descr[1] != 'env':
  402. continue
  403. if self.config[key] != config[key]:
  404. msg = '[config changed] '
  405. config_changed = True
  406. break
  407. else:
  408. msg = ''
  409. # this value is not covered by the above loop because it is handled
  410. # specially by the config class
  411. if self.config.extensions != config.extensions:
  412. msg = '[extensions changed] '
  413. config_changed = True
  414. # the source and doctree directories may have been relocated
  415. self.srcdir = srcdir
  416. self.doctreedir = doctreedir
  417. self.find_files(config)
  418. self.config = config
  419. added, changed, removed = self.get_outdated_files(config_changed)
  420. # if files were added or removed, all documents with globbed toctrees
  421. # must be reread
  422. if added or removed:
  423. changed.update(self.glob_toctrees)
  424. msg += '%s added, %s changed, %s removed' % (len(added), len(changed),
  425. len(removed))
  426. def update_generator():
  427. self.app = app
  428. # clear all files no longer present
  429. for docname in removed:
  430. if app:
  431. app.emit('env-purge-doc', self, docname)
  432. self.clear_doc(docname)
  433. # read all new and changed files
  434. to_read = added | changed
  435. for docname in sorted(to_read):
  436. yield docname
  437. self.read_doc(docname, app=app)
  438. if config.master_doc not in self.all_docs:
  439. self.warn(None, 'master file %s not found' %
  440. self.doc2path(config.master_doc))
  441. self.app = None
  442. if app:
  443. app.emit('env-updated', self)
  444. return msg, len(added | changed), update_generator()
  445. def check_dependents(self, already):
  446. to_rewrite = self.assign_section_numbers()
  447. for docname in to_rewrite:
  448. if docname not in already:
  449. yield docname
  450. # --------- SINGLE FILE READING --------------------------------------------
  451. def warn_and_replace(self, error):
  452. """
  453. Custom decoding error handler that warns and replaces.
  454. """
  455. linestart = error.object.rfind('\n', 0, error.start)
  456. lineend = error.object.find('\n', error.start)
  457. if lineend == -1: lineend = len(error.object)
  458. lineno = error.object.count('\n', 0, error.start) + 1
  459. self.warn(self.docname, 'undecodable source characters, '
  460. 'replacing with "?": %r' %
  461. (error.object[linestart+1:error.start] + '>>>' +
  462. error.object[error.start:error.end] + '<<<' +
  463. error.object[error.end:lineend]), lineno)
  464. return (u'?', error.end)
  465. def read_doc(self, docname, src_path=None, save_parsed=True, app=None):
  466. """
  467. Parse a file and add/update inventory entries for the doctree.
  468. If srcpath is given, read from a different source file.
  469. """
  470. # remove all inventory entries for that file
  471. if app:
  472. app.emit('env-purge-doc', self, docname)
  473. self.clear_doc(docname)
  474. if src_path is None:
  475. src_path = self.doc2path(docname)
  476. if self.config.default_role:
  477. role_fn, messages = roles.role(self.config.default_role, english,
  478. 0, dummy_reporter)
  479. if role_fn:
  480. roles._roles[''] = role_fn
  481. else:
  482. self.warn(docname, 'default role %s not found' %
  483. self.config.default_role)
  484. self.docname = docname
  485. self.settings['input_encoding'] = self.config.source_encoding
  486. self.settings['trim_footnote_reference_space'] = \
  487. self.config.trim_footnote_reference_space
  488. codecs.register_error('sphinx', self.warn_and_replace)
  489. codecs.register_error('sphinx', self.warn_and_replace)
  490. class SphinxSourceClass(FileInput):
  491. def decode(self_, data):
  492. return data.decode(self_.encoding, 'sphinx')
  493. def read(self_):
  494. data = FileInput.read(self_)
  495. if app:
  496. arg = [data]
  497. app.emit('source-read', docname, arg)
  498. data = arg[0]
  499. if self.config.rst_epilog:
  500. return data + '\n' + self.config.rst_epilog + '\n'
  501. else:
  502. return data
  503. # publish manually
  504. pub = Publisher(reader=SphinxStandaloneReader(),
  505. writer=SphinxDummyWriter(),
  506. source_class=SphinxSourceClass,
  507. destination_class=NullOutput)
  508. pub.set_components(None, 'restructuredtext', None)
  509. pub.process_programmatic_settings(None, self.settings, None)
  510. pub.set_source(None, src_path)
  511. pub.set_destination(None, None)
  512. try:
  513. pub.publish()
  514. doctree = pub.document
  515. except UnicodeError, err:
  516. raise SphinxError(str(err))
  517. self.filter_messages(doctree)
  518. self.process_dependencies(docname, doctree)
  519. self.process_images(docname, doctree)
  520. self.process_downloads(docname, doctree)
  521. self.process_metadata(docname, doctree)
  522. self.create_title_from(docname, doctree)
  523. self.note_labels_from(docname, doctree)
  524. self.note_indexentries_from(docname, doctree)
  525. self.note_citations_from(docname, doctree)
  526. self.build_toc_from(docname, doctree)
  527. # store time of reading, used to find outdated files
  528. self.all_docs[docname] = time.time()
  529. if app:
  530. app.emit('doctree-read', doctree)
  531. # make it picklable
  532. doctree.reporter = None
  533. doctree.transformer = None
  534. doctree.settings.warning_stream = None
  535. doctree.settings.env = None
  536. doctree.settings.record_dependencies = None
  537. for metanode in doctree.traverse(MetaBody.meta):
  538. # docutils' meta nodes aren't picklable because the class is nested
  539. metanode.__class__ = addnodes.meta
  540. # cleanup
  541. self.docname = None
  542. self.currmodule = None
  543. self.currclass = None
  544. self.gloss_entries = set()
  545. if save_parsed:
  546. # save the parsed doctree
  547. doctree_filename = self.doc2path(docname, self.doctreedir,
  548. '.doctree')
  549. dirname = path.dirname(doctree_filename)
  550. if not path.isdir(dirname):
  551. os.makedirs(dirname)
  552. f = open(doctree_filename, 'wb')
  553. try:
  554. pickle.dump(doctree, f, pickle.HIGHEST_PROTOCOL)
  555. finally:
  556. f.close()
  557. else:
  558. return doctree
  559. def filter_messages(self, doctree):
  560. """
  561. Filter system messages from a doctree.
  562. """
  563. filterlevel = self.config.keep_warnings and 2 or 5
  564. for node in doctree.traverse(nodes.system_message):
  565. if node['level'] < filterlevel:
  566. node.parent.remove(node)
  567. def process_dependencies(self, docname, doctree):
  568. """
  569. Process docutils-generated dependency info.
  570. """
  571. cwd = os.getcwd()
  572. frompath = path.join(path.normpath(self.srcdir), 'dummy')
  573. deps = doctree.settings.record_dependencies
  574. if not deps:
  575. return
  576. for dep in deps.list:
  577. # the dependency path is relative to the working dir, so get
  578. # one relative to the srcdir
  579. relpath = relative_path(frompath,
  580. path.normpath(path.join(cwd, dep)))
  581. self.dependencies.setdefault(docname, set()).add(relpath)
  582. def process_downloads(self, docname, doctree):
  583. """
  584. Process downloadable file paths.
  585. """
  586. docdir = path.dirname(self.doc2path(docname, base=None))
  587. for node in doctree.traverse(addnodes.download_reference):
  588. targetname = node['reftarget']
  589. if targetname.startswith('/') or targetname.startswith(os.sep):
  590. # absolute
  591. filepath = targetname[1:]
  592. else:
  593. filepath = path.normpath(path.join(docdir, node['reftarget']))
  594. self.dependencies.setdefault(docname, set()).add(filepath)
  595. if not os.access(path.join(self.srcdir, filepath), os.R_OK):
  596. self.warn(docname, 'download file not readable: %s' % filepath,
  597. getattr(node, 'line', None))
  598. continue
  599. uniquename = self.dlfiles.add_file(docname, filepath)
  600. node['filename'] = uniquename
  601. def process_images(self, docname, doctree):
  602. """
  603. Process and rewrite image URIs.
  604. """
  605. docdir = path.dirname(self.doc2path(docname, base=None))
  606. for node in doctree.traverse(nodes.image):
  607. # Map the mimetype to the corresponding image. The writer may
  608. # choose the best image from these candidates. The special key * is
  609. # set if there is only single candidate to be used by a writer.
  610. # The special key ? is set for nonlocal URIs.
  611. node['candidates'] = candidates = {}
  612. imguri = node['uri']
  613. if imguri.find('://') != -1:
  614. self.warn(docname, 'nonlocal image URI found: %s' % imguri,
  615. node.line)
  616. candidates['?'] = imguri
  617. continue
  618. # imgpath is the image path *from srcdir*
  619. if imguri.startswith('/') or imguri.startswith(os.sep):
  620. # absolute path (= relative to srcdir)
  621. imgpath = path.normpath(imguri[1:])
  622. else:
  623. imgpath = path.normpath(path.join(docdir, imguri))
  624. # set imgpath as default URI
  625. node['uri'] = imgpath
  626. if imgpath.endswith(os.extsep + '*'):
  627. for filename in glob(path.join(self.srcdir, imgpath)):
  628. new_imgpath = relative_path(self.srcdir, filename)
  629. if filename.lower().endswith('.pdf'):
  630. candidates['application/pdf'] = new_imgpath
  631. elif filename.lower().endswith('.svg'):
  632. candidates['image/svg+xml'] = new_imgpath
  633. else:
  634. try:
  635. f = open(filename, 'rb')
  636. try:
  637. imgtype = imghdr.what(f)
  638. finally:
  639. f.close()
  640. except (OSError, IOError):
  641. self.warn(docname,
  642. 'image file %s not readable' % filename)
  643. if imgtype:
  644. candidates['image/' + imgtype] = new_imgpath
  645. else:
  646. candidates['*'] = imgpath
  647. # map image paths to unique image names (so that they can be put
  648. # into a single directory)
  649. for imgpath in candidates.itervalues():
  650. self.dependencies.setdefault(docname, set()).add(imgpath)
  651. if not os.access(path.join(self.srcdir, imgpath), os.R_OK):
  652. self.warn(docname, 'image file not readable: %s' % imgpath,
  653. node.line)
  654. continue
  655. self.images.add_file(docname, imgpath)
  656. def process_metadata(self, docname, doctree):
  657. """
  658. Process the docinfo part of the doctree as metadata.
  659. """
  660. self.metadata[docname] = md = {}
  661. try:
  662. docinfo = doctree[0]
  663. except IndexError:
  664. # probably an empty document
  665. return
  666. if docinfo.__class__ is not nodes.docinfo:
  667. # nothing to see here
  668. return
  669. for node in docinfo:
  670. if node.__class__ is nodes.author:
  671. # handled specially by docutils
  672. md['author'] = node.astext()
  673. elif node.__class__ is nodes.field:
  674. name, body = node
  675. md[name.astext()] = body.astext()
  676. del doctree[0]
  677. def create_title_from(self, docname, document):
  678. """
  679. Add a title node to the document (just copy the first section title),
  680. and store that title in the environment.
  681. """
  682. titlenode = nodes.title()
  683. longtitlenode = titlenode
  684. # explicit title set with title directive; use this only for
  685. # the <title> tag in HTML output
  686. if document.has_key('title'):
  687. longtitlenode = nodes.title()
  688. longtitlenode += nodes.Text(document['title'])
  689. # look for first section title and use that as the title
  690. for node in document.traverse(nodes.section):
  691. visitor = SphinxContentsFilter(document)
  692. node[0].walkabout(visitor)
  693. titlenode += visitor.get_entry_text()
  694. break
  695. else:
  696. # document has no title
  697. titlenode += nodes.Text('<no title>')
  698. self.titles[docname] = titlenode
  699. self.longtitles[docname] = longtitlenode
  700. def note_labels_from(self, docname, document):
  701. for name, explicit in document.nametypes.iteritems():
  702. if not explicit:
  703. continue
  704. labelid = document.nameids[name]
  705. if labelid is None:
  706. continue
  707. node = document.ids[labelid]
  708. if name.isdigit() or node.has_key('refuri') or \
  709. node.tagname.startswith('desc_'):
  710. # ignore footnote labels, labels automatically generated from a
  711. # link and description units
  712. continue
  713. if name in self.labels:
  714. self.warn(docname, 'duplicate label %s, ' % name +
  715. 'other instance in ' +
  716. self.doc2path(self.labels[name][0]),
  717. node.line)
  718. self.anonlabels[name] = docname, labelid
  719. if node.tagname == 'section':
  720. sectname = node[0].astext() # node[0] == title node
  721. elif node.tagname == 'figure':
  722. for n in node:
  723. if n.tagname == 'caption':
  724. sectname = n.astext()
  725. break
  726. else:
  727. continue
  728. else:
  729. # anonymous-only labels
  730. continue
  731. self.labels[name] = docname, labelid, sectname
  732. def note_indexentries_from(self, docname, document):
  733. entries = self.indexentries[docname] = []
  734. for node in document.traverse(addnodes.index):
  735. entries.extend(node['entries'])
  736. def note_citations_from(self, docname, document):
  737. for node in document.traverse(nodes.citation):
  738. label = node[0].astext()
  739. if ('citation', label) in self.reftargets:
  740. self.warn(docname, 'duplicate citation %s, ' % label +
  741. 'other instance in %s' % self.doc2path(
  742. self.reftargets['citation', label][0]), node.line)
  743. self.reftargets['citation', label] = (docname, node['ids'][0])
  744. def note_toctree(self, docname, toctreenode):
  745. """Note a TOC tree directive in a document and gather information about
  746. file relations from it."""
  747. if toctreenode['glob']:
  748. self.glob_toctrees.add(docname)
  749. if toctreenode.get('numbered'):
  750. self.numbered_toctrees.add(docname)
  751. includefiles = toctreenode['includefiles']
  752. for includefile in includefiles:
  753. # note that if the included file is rebuilt, this one must be
  754. # too (since the TOC of the included file could have changed)
  755. self.files_to_rebuild.setdefault(includefile, set()).add(docname)
  756. self.toctree_includes.setdefault(docname, []).extend(includefiles)
  757. def build_toc_from(self, docname, document):
  758. """Build a TOC from the doctree and store it in the inventory."""
  759. numentries = [0] # nonlocal again...
  760. try:
  761. maxdepth = int(self.metadata[docname].get('tocdepth', 0))
  762. except ValueError:
  763. maxdepth = 0
  764. def traverse_in_section(node, cls):
  765. """Like traverse(), but stay within the same section."""
  766. result = []
  767. if isinstance(node, cls):
  768. result.append(node)
  769. for child in node.children:
  770. if isinstance(child, nodes.section):
  771. continue
  772. result.extend(traverse_in_section(child, cls))
  773. return result
  774. def build_toc(node, depth=1):
  775. entries = []
  776. for sectionnode in node:
  777. # find all toctree nodes in this section and add them
  778. # to the toc (just copying the toctree node which is then
  779. # resolved in self.get_and_resolve_doctree)
  780. if not isinstance(sectionnode, nodes.section):
  781. for toctreenode in traverse_in_section(sectionnode,
  782. addnodes.toctree):
  783. item = toctreenode.copy()
  784. entries.append(item)
  785. # important: do the inventory stuff
  786. self.note_toctree(docname, toctreenode)
  787. continue
  788. title = sectionnode[0]
  789. # copy the contents of the section title, but without references
  790. # and unnecessary stuff
  791. visitor = SphinxContentsFilter(document)
  792. title.walkabout(visitor)
  793. nodetext = visitor.get_entry_text()
  794. if not numentries[0]:
  795. # for the very first toc entry, don't add an anchor
  796. # as it is the file's title anyway
  797. anchorname = ''
  798. else:
  799. anchorname = '#' + sectionnode['ids'][0]
  800. numentries[0] += 1
  801. reference = nodes.reference('', '', refuri=docname,
  802. anchorname=anchorname,
  803. *nodetext)
  804. para = addnodes.compact_paragraph('', '', reference)
  805. item = nodes.list_item('', para)
  806. if maxdepth == 0 or depth < maxdepth:
  807. item += build_toc(sectionnode, depth+1)
  808. entries.append(item)
  809. if entries:
  810. return nodes.bullet_list('', *entries)
  811. return []
  812. toc = build_toc(document)
  813. if toc:
  814. self.tocs[docname] = toc
  815. else:
  816. self.tocs[docname] = nodes.bullet_list('')
  817. self.toc_num_entries[docname] = numentries[0]
  818. def get_toc_for(self, docname):
  819. """Return a TOC nodetree -- for use on the same page only!"""
  820. toc = self.tocs[docname].deepcopy()
  821. for node in toc.traverse(nodes.reference):
  822. node['refuri'] = node['anchorname']
  823. return toc
  824. def get_toctree_for(self, docname, builder, collapse):
  825. """Return the global TOC nodetree."""
  826. doctree = self.get_doctree(self.config.master_doc)
  827. for toctreenode in doctree.traverse(addnodes.toctree):
  828. result = self.resolve_toctree(docname, builder, toctreenode,
  829. prune=True, collapse=collapse)
  830. if result is not None:
  831. return result
  832. # -------
  833. # these are called from docutils directives and therefore use self.docname
  834. #
  835. def note_descref(self, fullname, desctype, line):
  836. if fullname in self.descrefs:
  837. self.warn(self.docname,
  838. 'duplicate canonical description name %s, ' % fullname +
  839. 'other instance in ' +
  840. self.doc2path(self.descrefs[fullname][0]),
  841. line)
  842. self.descrefs[fullname] = (self.docname, desctype)
  843. def note_module(self, modname, synopsis, platform, deprecated):
  844. self.modules[modname] = (self.docname, synopsis, platform, deprecated)
  845. self.filemodules.setdefault(self.docname, []).append(modname)
  846. def note_progoption(self, optname, labelid):
  847. self.progoptions[self.currprogram, optname] = (self.docname, labelid)
  848. def note_reftarget(self, type, name, labelid):
  849. self.reftargets[type, name] = (self.docname, labelid)
  850. def note_versionchange(self, type, version, node, lineno):
  851. self.versionchanges.setdefault(version, []).append(
  852. (type, self.docname, lineno, self.currmodule, self.currdesc,
  853. node.astext()))
  854. def note_dependency(self, filename):
  855. self.dependencies.setdefault(self.docname, set()).add(filename)
  856. # -------
  857. # --------- RESOLVING REFERENCES AND TOCTREES ------------------------------
  858. def get_doctree(self, docname):
  859. """Read the doctree for a file from the pickle and return it."""
  860. doctree_filename = self.doc2path(docname, self.doctreedir, '.doctree')
  861. f = open(doctree_filename, 'rb')
  862. try:
  863. doctree = pickle.load(f)
  864. finally:
  865. f.close()
  866. doctree.settings.env = self
  867. doctree.reporter = Reporter(self.doc2path(docname), 2, 4,
  868. stream=WarningStream(self._warnfunc))
  869. return doctree
  870. def get_and_resolve_doctree(self, docname, builder, doctree=None,
  871. prune_toctrees=True):
  872. """Read the doctree from the pickle, resolve cross-references and
  873. toctrees and return it."""
  874. if doctree is None:
  875. doctree = self.get_doctree(docname)
  876. # resolve all pending cross-references
  877. self.resolve_references(doctree, docname, builder)
  878. # now, resolve all toctree nodes
  879. for toctreenode in doctree.traverse(addnodes.toctree):
  880. result = self.resolve_toctree(docname, builder, toctreenode,
  881. prune=prune_toctrees)
  882. if result is None:
  883. toctreenode.replace_self([])
  884. else:
  885. toctreenode.replace_self(result)
  886. return doctree
  887. def resolve_toctree(self, docname, builder, toctree, prune=True, maxdepth=0,
  888. titles_only=False, collapse=False):
  889. """
  890. Resolve a *toctree* node into individual bullet lists with titles
  891. as items, returning None (if no containing titles are found) or
  892. a new node.
  893. If *prune* is True, the tree is pruned to *maxdepth*, or if that is 0,
  894. to the value of the *maxdepth* option on the *toctree* node.
  895. If *titles_only* is True, only toplevel document titles will be in the
  896. resulting tree.
  897. If *collapse* is True, all branches not containing docname will
  898. be collapsed.
  899. """
  900. if toctree.get('hidden', False):
  901. return None
  902. def _walk_depth(node, depth, maxdepth):
  903. """Utility: Cut a TOC at a specified depth."""
  904. for subnode in node.children[:]:
  905. if isinstance(subnode, (addnodes.compact_paragraph,
  906. nodes.list_item)):
  907. subnode['classes'].append('toctree-l%d' % (depth-1))
  908. _walk_depth(subnode, depth, maxdepth)
  909. elif isinstance(subnode, nodes.bullet_list):
  910. if maxdepth > 0 and depth > maxdepth:
  911. subnode.parent.replace(subnode, [])
  912. else:
  913. _walk_depth(subnode, depth+1, maxdepth)
  914. # cull sub-entries whose parents aren't 'current'
  915. if (collapse and
  916. depth > 1 and
  917. 'current' not in subnode.parent['classes']):
  918. subnode.parent.remove(subnode)
  919. elif isinstance(subnode, nodes.reference):
  920. # identify the toc entry pointing to the current document
  921. if subnode['refuri'] == docname and \
  922. not subnode['anchorname']:
  923. # tag the whole branch as 'current'
  924. p = subnode
  925. while p:
  926. p['classes'].append('current')
  927. p = p.parent
  928. def _entries_from_toctree(toctreenode, separate=False, subtree=False):
  929. """Return TOC entries for a toctree node."""
  930. refs = [(e[0], str(e[1])) for e in toctreenode['entries']]
  931. entries = []
  932. for (title, ref) in refs:
  933. try:
  934. if url_re.match(ref):
  935. reference = nodes.reference('', '',
  936. refuri=ref, anchorname='',
  937. *[nodes.Text(title)])
  938. para = addnodes.compact_paragraph('', '', reference)
  939. item = nodes.list_item('', para)
  940. toc = nodes.bullet_list('', item)
  941. elif ref == 'self':
  942. # 'self' refers to the document from which this
  943. # toctree originates
  944. ref = toctreenode['parent']
  945. if not title:
  946. title = self.titles[ref].astext()
  947. reference = nodes.reference('', '',
  948. refuri=ref,
  949. anchorname='',
  950. *[nodes.Text(title)])
  951. para = addnodes.compact_paragraph('', '', reference)
  952. item = nodes.list_item('', para)
  953. # don't show subitems
  954. toc = nodes.bullet_list('', item)
  955. else:
  956. toc = self.tocs[ref].deepcopy()
  957. if title and toc.children and len(toc.children) == 1:
  958. child = toc.children[0]
  959. for refnode in child.traverse(nodes.reference):
  960. if refnode['refuri'] == ref and \
  961. not refnode['anchorname']:
  962. refnode.children = [nodes.Text(title)]
  963. if not toc.children:
  964. # empty toc means: no titles will show up in the toctree
  965. self.warn(docname,
  966. 'toctree contains reference to document '
  967. '%r that doesn\'t have a title: no link '
  968. 'will be generated' % ref)
  969. except KeyError:
  970. # this is raised if the included file does not exist
  971. self.warn(docname, 'toctree contains reference to '
  972. 'nonexisting document %r' % ref)
  973. else:
  974. # if titles_only is given, only keep the main title and
  975. # sub-toctrees
  976. if titles_only:
  977. # delete everything but the toplevel title(s)
  978. # and toctrees
  979. for toplevel in toc:
  980. # nodes with length 1 don't have any children anyway
  981. if len(toplevel) > 1:
  982. subtrees = toplevel.traverse(addnodes.toctree)
  983. toplevel[1][:] = subtrees
  984. # resolve all sub-toctrees
  985. for toctreenode in toc.traverse(addnodes.toctree):
  986. i = toctreenode.parent.index(toctreenode) + 1
  987. for item in _entries_from_toctree(toctreenode,
  988. subtree=True):
  989. toctreenode.parent.insert(i, item)
  990. i += 1
  991. toctreenode.parent.remove(toctreenode)
  992. if separate:
  993. entries.append(toc)
  994. else:
  995. entries.extend(toc.children)
  996. if not subtree and not separate:
  997. ret = nodes.bullet_list()
  998. ret += entries
  999. return [ret]
  1000. return entries
  1001. maxdepth = maxdepth or toctree.get('maxdepth', -1)
  1002. # NOTE: previously, this was separate=True, but that leads to artificial
  1003. # separation when two or more toctree entries form a logical unit, so
  1004. # separating mode is no longer used -- it's kept here for history's sake
  1005. tocentries = _entries_from_toctree(toctree, separate=False)
  1006. if not tocentries:
  1007. return None
  1008. newnode = addnodes.compact_paragraph('', '', *tocentries)
  1009. newnode['toctree'] = True
  1010. # prune the tree to maxdepth and replace titles, also set level classes
  1011. _walk_depth(newnode, 1, prune and maxdepth or 0)
  1012. # set the target paths in the toctrees (they are not known at TOC
  1013. # generation time)
  1014. for refnode in newnode.traverse(nodes.reference):
  1015. if not url_re.match(refnode['refuri']):
  1016. refnode['refuri'] = builder.get_relative_uri(
  1017. docname, refnode['refuri']) + refnode['anchorname']
  1018. return newnode
  1019. descroles = frozenset(('data', 'exc', 'func', 'class', 'const',
  1020. 'attr', 'obj', 'meth', 'cfunc', 'cmember',
  1021. 'cdata', 'ctype', 'cmacro'))
  1022. def resolve_references(self, doctree, fromdocname, builder):
  1023. reftarget_roles = set(('token', 'term', 'citation'))
  1024. # add all custom xref types too
  1025. reftarget_roles.update(i[0] for i in additional_xref_types.values())
  1026. for node in doctree.traverse(addnodes.pending_xref):
  1027. contnode = node[0].deepcopy()
  1028. newnode = None
  1029. typ = node['reftype']
  1030. target = node['reftarget']
  1031. try:
  1032. if typ == 'ref':
  1033. if node['refcaption']:
  1034. # reference to anonymous label; the reference uses
  1035. # the supplied link caption
  1036. docname, labelid = self.anonlabels.get(target, ('',''))
  1037. sectname = node.astext()
  1038. if not docname:
  1039. self.warn(fromdocname, 'undefined label: %s' %
  1040. target, node.line)
  1041. else:
  1042. # reference to the named label; the final node will
  1043. # contain the section name after the label
  1044. docname, labelid, sectname = self.labels.get(target,
  1045. ('','',''))
  1046. if not docname:
  1047. self.warn(
  1048. fromdocname,
  1049. 'undefined label: %s' % target + ' -- if you '
  1050. 'don\'t give a link caption the label must '
  1051. 'precede a section header.', node.line)
  1052. if docname:
  1053. newnode = nodes.reference('', '')
  1054. innernode = nodes.emphasis(sectname, sectname)
  1055. if docname == fromdocname:
  1056. newnode['refid'] = labelid
  1057. else:
  1058. # set more info in contnode; in case the
  1059. # get_relative_uri call raises NoUri,
  1060. # the builder will then have to resolve these
  1061. contnode = addnodes.pending_xref('')
  1062. contnode['refdocname'] = docname
  1063. contnode['refsectname'] = sectname
  1064. newnode['refuri'] = builder.get_relative_uri(
  1065. fromdocname, docname)
  1066. if labelid:
  1067. newnode['refuri'] += '#' + labelid
  1068. newnode.append(innernode)
  1069. else:
  1070. newnode = contnode
  1071. elif typ == 'doc':
  1072. # directly reference to document by source name;
  1073. # can be absolute or relative
  1074. docname = docname_join(fromdocname, target)
  1075. if docname not in self.all_docs:
  1076. self.warn(fromdocname, 'unknown document: %s' % docname,
  1077. node.line)
  1078. newnode = contnode
  1079. else:
  1080. if node['refcaption']:
  1081. # reference with explicit title
  1082. caption = node.astext()
  1083. else:
  1084. caption = self.titles[docname].astext()
  1085. innernode = nodes.emphasis(caption, caption)
  1086. newnode = nodes.reference('', '')
  1087. newnode['refuri'] = builder.get_relative_uri(
  1088. fromdocname, docname)
  1089. newnode.append(innernode)
  1090. elif typ == 'keyword':
  1091. # keywords are referenced by named labels
  1092. docname, labelid, _ = self.labels.get(target, ('','',''))
  1093. if not docname:
  1094. #self.warn(fromdocname, 'unknown keyword: %s' % target)
  1095. newnode = contnode
  1096. else:
  1097. newnode = nodes.reference('', '')
  1098. if docname == fromdocname:
  1099. newnode['refid'] = labelid
  1100. else:
  1101. newnode['refuri'] = builder.get_relative_uri(
  1102. fromdocname, docname) + '#' + labelid
  1103. newnode.append(contnode)
  1104. elif typ == 'option':
  1105. progname = node['refprogram']
  1106. docname, labelid = self.progoptions.get((progname, target),
  1107. ('', ''))
  1108. if not docname:
  1109. newnode = contnode
  1110. else:
  1111. newnode = nodes.reference('', '')
  1112. if docname == fromdocname:
  1113. newnode['refid'] = labelid
  1114. else:
  1115. newnode['refuri'] = builder.get_relative_uri(
  1116. fromdocname, docname) + '#' + labelid
  1117. newnode.append(contnode)
  1118. elif typ in reftarget_roles:
  1119. docname, labelid = self.reftargets.get((typ, target),
  1120. ('', ''))
  1121. if not docname:
  1122. if typ == 'term':
  1123. self.warn(fromdocname,
  1124. 'term not in glossary: %s' % target,
  1125. node.line)
  1126. elif typ == 'citation':
  1127. self.warn(fromdocname,
  1128. 'citation not found: %s' % target,
  1129. node.line)
  1130. newnode = contnode
  1131. else:
  1132. newnode = nodes.reference('', '')
  1133. if docname == fromdocname:
  1134. newnode['refid'] = labelid
  1135. else:
  1136. newnode['refuri'] = builder.get_relative_uri(
  1137. fromdocname, docname, typ) + '#' + labelid
  1138. newnode.append(contnode)
  1139. elif typ == 'mod' or \
  1140. typ == 'obj' and target in self.modules:
  1141. docname, synopsis, platform, deprecated = \
  1142. self.modules.get(target, ('','','', ''))
  1143. if not docname:
  1144. newnode = builder.app.emit_firstresult(
  1145. 'missing-reference', self, node, contnode)
  1146. if not newnode:
  1147. newnode = contnode
  1148. elif docname == fromdocname:
  1149. # don't link to self
  1150. newnode = contnode
  1151. else:
  1152. newnode = nodes.reference('', '')
  1153. newnode['refuri'] = builder.get_relative_uri(
  1154. fromdocname, docname) + '#module-' + target
  1155. newnode['reftitle'] = '%s%s%s' % (
  1156. (platform and '(%s) ' % platform),
  1157. synopsis, (deprecated and ' (deprecated)' or ''))
  1158. newnode.append(contnode)
  1159. elif typ in self.descroles:
  1160. # "descrefs"
  1161. modname = node['modname']
  1162. clsname = node['classname']
  1163. searchorder = node.hasattr('refspecific') and 1 or 0
  1164. name, desc = self.find_desc(modname, clsname,
  1165. target, typ, searchorder)
  1166. if not desc:
  1167. newnode = builder.app.emit_firstresult(
  1168. 'missing-reference', self, node, contnode)
  1169. if not newnode:
  1170. newnode = contnode
  1171. else:
  1172. newnode = nodes.reference('', '')
  1173. if desc[0] == fromdocname:
  1174. newnode['refid'] = name
  1175. else:
  1176. newnode['refuri'] = (
  1177. builder.get_relative_uri(fromdocname, desc[0])
  1178. + '#' + name)
  1179. newnode['reftitle'] = name
  1180. newnode.append(contnode)
  1181. else:
  1182. raise RuntimeError('unknown xfileref node encountered: %s'
  1183. % node)
  1184. except NoUri:
  1185. newnode = contnode
  1186. if newnode:
  1187. node.replace_self(newnode)
  1188. for node in doctree.traverse(addnodes.only):
  1189. try:
  1190. ret = builder.tags.eval_condition(node['expr'])
  1191. except Exception, err:
  1192. self.warn(fromdocname, 'exception while evaluating only '
  1193. 'directive expression: %s' % err, node.line)
  1194. node.replace_self(node.children)
  1195. else:
  1196. if ret:
  1197. node.replace_self(node.children)
  1198. else:
  1199. node.replace_self([])
  1200. # allow custom references to be resolved
  1201. builder.app.emit('doctree-resolved', doctree, fromdocname)
  1202. def assign_section_numbers(self):
  1203. """Assign a section number to each heading under a numbered toctree."""
  1204. # a list of all docnames whose section numbers changed
  1205. rewrite_needed = []
  1206. old_secnumbers = self.toc_secnumbers
  1207. self.toc_secnumbers = {}
  1208. def _walk_toc(node, secnums, titlenode=None):
  1209. # titlenode is the title of the document, it will get assigned a
  1210. # secnumber too, so that it shows up in next/prev/parent rellinks
  1211. for subnode in node.children:
  1212. if isinstance(subnode, nodes.bullet_list):
  1213. numstack.append(0)
  1214. _walk_toc(subnode, secnums, titlenode)
  1215. numstack.pop()
  1216. titlenode = None
  1217. elif isinstance(subnode, nodes.list_item):
  1218. _walk_toc(subnode, secnums, titlenode)
  1219. titlenode = None
  1220. elif isinstance(subnode, addnodes.compact_paragraph):
  1221. numstack[-1] += 1
  1222. secnums[subnode[0]['anchorname']] = \
  1223. subnode[0]['secnumber'] = tuple(numstack)
  1224. if titlenode:
  1225. titlenode['secnumber'] = tuple(numstack)
  1226. titlenode = None
  1227. elif isinstance(subnode, addnodes.toctree):
  1228. _walk_toctree(subnode)
  1229. def _walk_toctree(toctreenode):
  1230. for (title, ref) in toctreenode['entries']:
  1231. if url_re.match(ref) or ref == 'self':
  1232. # don't mess with those
  1233. continue
  1234. if ref in self.tocs:
  1235. secnums = self.toc_secnumbers[ref] = {}
  1236. _walk_toc(self.tocs[ref], secnums, self.titles.get(ref))
  1237. if secnums != old_secnumbers.get(ref):
  1238. rewrite_needed.append(ref)
  1239. for docname in self.numbered_toctrees:
  1240. doctree = self.get_doctree(docname)
  1241. for toctreenode in doctree.traverse(addnodes.toctree):
  1242. if toctreenode.get('numbered'):
  1243. # every numbered toctree gets new numbering
  1244. numstack = [0]
  1245. _walk_toctree(toctreenode)
  1246. return rewrite_needed
  1247. def create_index(self, builder, _fixre=re.compile(r'(.*) ([(][^()]*[)])')):
  1248. """Create the real index from the collected index entries."""
  1249. new = {}
  1250. def add_entry(word, subword, dic=new):
  1251. entry = dic.get(word)
  1252. if not entry:
  1253. dic[word] = entry = [[], {}]
  1254. if subword:
  1255. add_entry(subword, '', dic=entry[1])
  1256. else:
  1257. try:
  1258. entry[0].append(builder.get_relative_uri('genindex', fn)
  1259. + '#' + tid)
  1260. except NoUri:
  1261. pass
  1262. for fn, entries in self.indexentries.iteritems():
  1263. # new entry types must be listed in directives/other.py!
  1264. for type, value, tid, alias in entries:
  1265. if type == 'single':
  1266. try:
  1267. entry, subentry = value.split(';', 1)
  1268. except ValueError:
  1269. entry, subentry = value, ''
  1270. if not entry:
  1271. self.warn(fn, 'invalid index entry %r' % value)
  1272. continue
  1273. add_entry(entry.strip(), subentry.strip())
  1274. elif type == 'pair':
  1275. try:
  1276. first, second = map(lambda x: x.strip(),
  1277. value.split(';', 1))
  1278. if not first or not second:
  1279. raise ValueError
  1280. except ValueError:
  1281. self.warn(fn, 'invalid pair index entry %r' % value)
  1282. continue
  1283. add_entry(first, second)
  1284. add_entry(second, first)
  1285. elif type == 'triple':
  1286. try:
  1287. first, second, third = map(lambda x: x.strip(),
  1288. value.split(';', 2))
  1289. if not first or not second or not third:
  1290. raise ValueError
  1291. except ValueError:
  1292. self.warn(fn, 'invalid triple index entry %r' % value)
  1293. continue
  1294. add_entry(first, second+' '+third)
  1295. add_entry(second, third+', '+first)
  1296. add_entry(third, first+' '+second)
  1297. else:
  1298. self.warn(fn, 'unknown index entry type %r' % type)
  1299. # sort the index entries; put all symbols at the front, even those
  1300. # following the letters in ASCII, this is where the chr(127) comes from
  1301. def keyfunc(entry, lcletters=string.ascii_lowercase + '_'):
  1302. lckey = entry[0].lower()
  1303. if lckey[0:1] in lcletters:
  1304. return chr(127) + lckey
  1305. return lckey
  1306. newlist = new.items()
  1307. newlist.sort(key=keyfunc)
  1308. # fixup entries: transform
  1309. # func() (in module foo)
  1310. # func() (in module bar)
  1311. # into
  1312. # func()
  1313. # (in module foo)
  1314. # (in module bar)
  1315. oldkey = ''
  1316. oldsubitems = None
  1317. i = 0
  1318. while i < len(newlist):
  1319. key, (targets, subitems) = newlist[i]
  1320. # cannot move if it has subitems; structure gets too complex
  1321. if not subitems:
  1322. m = _fixre.match(key)
  1323. if m:
  1324. if oldkey == m.group(1):
  1325. # prefixes match: add entry as subitem of the
  1326. # previous entry
  1327. oldsubitems.setdefault(m.group(2), [[], {}])[0].\
  1328. extend(targets)
  1329. del newlist[i]
  1330. continue
  1331. oldkey = m.group(1)
  1332. else:
  1333. oldkey = key
  1334. oldsubitems = subitems
  1335. i += 1
  1336. # group the entries by letter
  1337. def keyfunc((k, v), letters=string.ascii_uppercase + '_'):
  1338. # hack: mutating the subitems dicts to a list in the keyfunc
  1339. v[1] = sorted((si, se) for (si, (se, void)) in v[1].iteritems())
  1340. # now calculate the key
  1341. letter = k[0].upper()
  1342. if letter in letters:
  1343. return letter
  1344. else:
  1345. # get all other symbols under one heading
  1346. return 'Symbols'
  1347. return [(key, list(group))
  1348. for (key, group) in groupby(newlist, keyfunc)]
  1349. def collect_relations(self):
  1350. relations = {}
  1351. getinc = self.toctree_includes.get
  1352. def collect(parents, docname, previous, next):
  1353. includes = getinc(docname)
  1354. # previous
  1355. if not previous:
  1356. # if no previous sibling, go to parent
  1357. previous = parents[0][0]
  1358. else:
  1359. # else, go to previous sibling, or if it has children, to
  1360. # the last of its children, or if that has children, to the
  1361. # last of those, and so forth
  1362. while 1:
  1363. previncs = getinc(previous)
  1364. if previncs:
  1365. previous = previncs[-1]
  1366. else:
  1367. break
  1368. # next
  1369. if includes:
  1370. # if it has children, go to first of them
  1371. next = includes[0]
  1372. elif next:
  1373. # else, if next sibling, go to it
  1374. pass
  1375. else:
  1376. # else, go to the next sibling of the parent, if present,
  1377. # else the grandparent's sibling, if present, and so forth
  1378. for parname, parindex in parents:
  1379. parincs = getinc(parname)
  1380. if parincs and parindex + 1 < len(parincs):
  1381. next = parincs[parindex+1]
  1382. break
  1383. # else it will stay None
  1384. # same for children
  1385. if includes:
  1386. for subindex, args in enumerate(izip(includes,
  1387. [None] + includes,
  1388. includes[1:] + [None])):
  1389. collect([(docname, subindex)] + parents, *args)
  1390. relations[docname] = [parents[0][0], previous, next]
  1391. collect([(None, 0)], self.config.master_doc, None, None)
  1392. return relations
  1393. def check_consistency(self):
  1394. """Do consistency checks."""
  1395. for docname in sorted(self.all_docs):
  1396. if docname not in self.files_to_rebuild:
  1397. if docname == self.config.master_doc:
  1398. # the master file is not included anywhere ;)
  1399. continue
  1400. self.warn(docname, 'document isn\'t included in any toctree')
  1401. # --------- QUERYING -------------------------------------------------------
  1402. def find_desc(self, modname, classname, name, type, searchorder=0):
  1403. """Find a description node matching "name", perhaps using
  1404. the given module and/or classname."""
  1405. # skip parens
  1406. if name[-2:] == '()':
  1407. name = name[:-2]
  1408. if not name:
  1409. return None, None
  1410. # don't add module and class names for C things
  1411. if type[0] == 'c' and type not in ('class', 'const'):
  1412. # skip trailing star and whitespace
  1413. name = name.rstrip(' *')
  1414. if name in self.descrefs and self.descrefs[name][1][0] == 'c':
  1415. return name, self.descrefs[name]
  1416. return None, None
  1417. newname = None
  1418. if searchorder == 1:
  1419. if modname and classname and \
  1420. modname + '.' + classname + '.' + name in self.descrefs:
  1421. newname = modname + '.' + classname + '.' + name
  1422. elif modname and modname + '.' + name in self.descrefs:
  1423. newname = modname + '.' + name
  1424. elif name in self.descrefs:
  1425. newname = name
  1426. else:
  1427. if name in self.descrefs:
  1428. newname = name
  1429. elif modname and modname + '.' + name in self.descrefs:
  1430. newname = modname + '.' + name
  1431. elif modname and classname and \
  1432. modname + '.' + classname + '.' + name in self.descrefs:
  1433. newname = modname + '.' + classname + '.' + name
  1434. # special case: builtin exceptions have module "exceptions" set
  1435. elif type == 'exc' and '.' not in name and \
  1436. 'exceptions.' + name in self.descrefs:
  1437. newname = 'exceptions.' + name
  1438. # special case: object methods
  1439. elif type in ('func', 'meth') and '.' not in name and \
  1440. 'object.' + name in self.descrefs:
  1441. newname = 'object.' + name
  1442. if newname is None:
  1443. return None, None
  1444. return newname, self.descrefs[newname]
  1445. def find_keyword(self, keyword, avoid_fuzzy=False, cutoff=0.6, n=20):
  1446. """
  1447. Find keyword matches for a keyword. If there's an exact match,
  1448. just return it, else return a list of fuzzy matches if avoid_fuzzy
  1449. isn't True.
  1450. Keywords searched are: first modules, then descrefs.
  1451. Returns: None if nothing found
  1452. (type, docname, anchorname) if exact match found
  1453. list of (quality, type, docname, anchorname, description)
  1454. if fuzzy
  1455. """
  1456. if keyword in self.modules:
  1457. docname, title, system, deprecated = self.modules[keyword]
  1458. return 'module', docname, 'module-' + keyword
  1459. if keyword in self.descrefs:
  1460. docname, ref_type = self.descrefs[keyword]
  1461. return ref_type, docname, keyword
  1462. # special cases
  1463. if '.' not in keyword:
  1464. # exceptions are documented in the exceptions module
  1465. if 'exceptions.'+keyword in self.descrefs:
  1466. docname, ref_type = self.descrefs['exceptions.'+keyword]
  1467. return ref_type, docname, 'exceptions.'+keyword
  1468. # special methods are documented as object methods
  1469. if 'object.'+keyword in self.descrefs:
  1470. docname, ref_type = self.descrefs['object.'+keyword]
  1471. return ref_type, docname, 'object.'+keyword
  1472. if avoid_fuzzy:
  1473. return
  1474. # find fuzzy matches
  1475. s = difflib.SequenceMatcher()
  1476. s.set_seq2(keyword.lower())
  1477. def possibilities():
  1478. for title, (fn, desc, _, _) in self.modules.iteritems():
  1479. yield ('module', fn, 'module-'+title, desc)
  1480. for title, (fn, desctype) in self.descrefs.iteritems():
  1481. yield (desctype, fn, title, '')
  1482. def dotsearch(string):
  1483. parts = string.lower().split('.')
  1484. for idx in xrange(0, len(parts)):
  1485. yield '.'.join(parts[idx:])
  1486. result = []
  1487. for type, docname, title, desc in possibilities():
  1488. best_res = 0
  1489. for part in dotsearch(title):
  1490. s.set_seq1(part)
  1491. if s.real_quick_ratio() >= cutoff and \
  1492. s.quick_ratio() >= cutoff and \
  1493. s.ratio() >= cutoff and \
  1494. s.ratio() > best_res:
  1495. best_res = s.ratio()
  1496. if best_res:
  1497. result.append((best_res, type, docname, title, desc))
  1498. return heapq.nlargest(n, result)