PageRenderTime 54ms CodeModel.GetById 17ms 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

Large files files are truncated, but you can click here to view the full file

  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

Large files files are truncated, but you can click here to view the full file