def register_sections_as_label(app: Sphinx, document: Node) -> None: domain = cast(StandardDomain, app.env.get_domain('std')) for node in document.findall(nodes.section): if (app.config.autosectionlabel_maxdepth and get_node_depth(node) >= app.config.autosectionlabel_maxdepth): continue labelid = node['ids'][0] docname = app.env.docname title = cast(nodes.title, node[0]) ref_name = getattr(title, 'rawsource', title.astext()) if app.config.autosectionlabel_prefix_document: name = nodes.fully_normalize_name(docname + ':' + ref_name) else: name = nodes.fully_normalize_name(ref_name) sectname = clean_astext(title) if name in domain.labels: logger.warning(__('duplicate label %s, other instance in %s'), name, app.env.doc2path(domain.labels[name][0]), location=node, type='autosectionlabel', subtype=docname) domain.anonlabels[name] = docname, labelid domain.labels[name] = docname, labelid, sectname
def post_process_images(self, doctree: Node) -> None: """Pick the best candidate for all image URIs.""" images = ImageAdapter(self.env) for node in doctree.findall(nodes.image): if '?' in node['candidates']: # don't rewrite nonlocal image URIs continue if '*' not in node['candidates']: for imgtype in self.supported_image_types: candidate = node['candidates'].get(imgtype, None) if candidate: break else: mimetypes = sorted(node['candidates']) image_uri = images.get_original_image_uri(node['uri']) if mimetypes: logger.warning(__('a suitable image for %s builder not found: ' '%s (%s)'), self.name, mimetypes, image_uri, location=node) else: logger.warning(__('a suitable image for %s builder not found: %s'), self.name, image_uri, location=node) continue node['uri'] = candidate else: candidate = node['uri'] if candidate not in self.env.images: # non-existing URI; let it alone continue self.images[candidate] = self.env.images[candidate][1]
def write_doc(self, docname: str, doctree: Node) -> None: # work around multiple string % tuple issues in docutils; # replace tuples in attribute values with lists doctree = doctree.deepcopy() for domain in self.env.domains.values(): xmlns = "xmlns:" + domain.name doctree[xmlns] = "https://www.sphinx-doc.org/" # type: ignore for node in doctree.findall(nodes.Element): for att, value in node.attributes.items(): if isinstance(value, tuple): node.attributes[att] = list(value) value = node.attributes[att] if isinstance(value, list): for i, val in enumerate(value): if isinstance(val, tuple): value[i] = list(val) destination = StringOutput(encoding='utf-8') self.writer.write(doctree, destination) outfilename = path.join(self.outdir, os_path(docname) + self.out_suffix) ensuredir(path.dirname(outfilename)) try: with open(outfilename, 'w', encoding='utf-8') as f: f.write(self.writer.output) except OSError as err: logger.warning(__("error writing file %s: %s"), outfilename, err)
def add_uids(doctree: Node, condition: Any) -> Iterator[Node]: """Add a unique id to every node in the `doctree` which matches the condition and yield the nodes. :param doctree: A :class:`docutils.nodes.document` instance. :param condition: A callable which returns either ``True`` or ``False`` for a given node. """ for node in doctree.findall(condition): node.uid = uuid4().hex yield node
def process_doc(self, env: BuildEnvironment, docname: str, document: Node) -> None: """Process a document after it is read by the environment.""" entries = self.entries.setdefault(env.docname, []) for node in list(document.findall(addnodes.index)): try: for entry in node['entries']: split_index_msg(entry[0], entry[1]) except ValueError as exc: logger.warning(str(exc), location=node) node.parent.remove(node) else: for entry in node['entries']: entries.append(entry)
def fix_refuris(self, tree: Node) -> None: # fix refuris with double anchor fname = self.config.root_doc + self.out_suffix for refnode in tree.findall(nodes.reference): if 'refuri' not in refnode: continue refuri = refnode['refuri'] hashindex = refuri.find('#') if hashindex < 0: continue hashindex = refuri.find('#', hashindex + 1) if hashindex >= 0: refnode['refuri'] = fname + refuri[hashindex:]
def doctree_read(app: Sphinx, doctree: Node) -> None: env = app.builder.env resolve_target = getattr(env.config, 'linkcode_resolve', None) if not callable(env.config.linkcode_resolve): raise LinkcodeError( "Function `linkcode_resolve` is not given in conf.py") domain_keys = { 'py': ['module', 'fullname'], 'c': ['names'], 'cpp': ['names'], 'js': ['object', 'fullname'], } for objnode in list(doctree.findall(addnodes.desc)): domain = objnode.get('domain') uris: Set[str] = set() for signode in objnode: if not isinstance(signode, addnodes.desc_signature): continue # Convert signode to a specified format info = {} for key in domain_keys.get(domain, []): value = signode.get(key) if not value: value = '' info[key] = value if not info: continue # Call user code to resolve the link uri = resolve_target(domain, info) if not uri: # no source continue if uri in uris or not uri: # only one link per name, please continue uris.add(uri) inline = nodes.inline('', _('[source]'), classes=['viewcode-link']) onlynode = addnodes.only(expr='html') onlynode += nodes.reference('', '', inline, internal=False, refuri=uri) signode += onlynode
def process_only_nodes(document: Node, tags: "Tags") -> None: """Filter ``only`` nodes which do not match *tags*.""" for node in document.findall(addnodes.only): try: ret = tags.eval_condition(node['expr']) except Exception as err: logger.warning(__('exception while evaluating only directive expression: %s'), err, location=node) node.replace_self(node.children or nodes.comment()) else: if ret: node.replace_self(node.children or nodes.comment()) else: # A comment on the comment() nodes being inserted: replacing by [] would # result in a "Losing ids" exception if there is a target node before # the only node, so we make sure docutils can transfer the id to # something, even if it's just a comment and will lose the id anyway... node.replace_self(nodes.comment())
def merge_doctrees(old: Node, new: Node, condition: Any) -> Iterator[Node]: """Merge the `old` doctree with the `new` one while looking at nodes matching the `condition`. Each node which replaces another one or has been added to the `new` doctree will be yielded. :param condition: A callable which returns either ``True`` or ``False`` for a given node. """ old_iter = old.findall(condition) new_iter = new.findall(condition) old_nodes = [] new_nodes = [] ratios = {} seen = set() # compare the nodes each doctree in order for old_node, new_node in zip_longest(old_iter, new_iter): if old_node is None: new_nodes.append(new_node) continue if not getattr(old_node, 'uid', None): # maybe config.gettext_uuid has been changed. old_node.uid = uuid4().hex if new_node is None: old_nodes.append(old_node) continue ratio = get_ratio(old_node.rawsource, new_node.rawsource) if ratio == 0: new_node.uid = old_node.uid seen.add(new_node) else: ratios[old_node, new_node] = ratio old_nodes.append(old_node) new_nodes.append(new_node) # calculate the ratios for each unequal pair of nodes, should we stumble # on a pair which is equal we set the uid and add it to the seen ones for old_node, new_node in product(old_nodes, new_nodes): if new_node in seen or (old_node, new_node) in ratios: continue ratio = get_ratio(old_node.rawsource, new_node.rawsource) if ratio == 0: new_node.uid = old_node.uid seen.add(new_node) else: ratios[old_node, new_node] = ratio # choose the old node with the best ratio for each new node and set the uid # as long as the ratio is under a certain value, in which case we consider # them not changed but different ratios = sorted(ratios.items(), key=itemgetter(1)) # type: ignore for (old_node, new_node), ratio in ratios: if new_node in seen: continue else: seen.add(new_node) if ratio < VERSIONING_RATIO: new_node.uid = old_node.uid else: new_node.uid = uuid4().hex yield new_node # create new uuids for any new node we left out earlier, this happens # if one or more nodes are simply added. for new_node in set(new_nodes) - seen: new_node.uid = uuid4().hex yield new_node
def test_doc(self, docname: str, doctree: Node) -> None: groups: Dict[str, TestGroup] = {} add_to_all_groups = [] self.setup_runner = SphinxDocTestRunner(verbose=False, optionflags=self.opt) self.test_runner = SphinxDocTestRunner(verbose=False, optionflags=self.opt) self.cleanup_runner = SphinxDocTestRunner(verbose=False, optionflags=self.opt) self.test_runner._fakeout = self.setup_runner._fakeout # type: ignore self.cleanup_runner._fakeout = self.setup_runner._fakeout # type: ignore if self.config.doctest_test_doctest_blocks: def condition(node: Node) -> bool: return (isinstance(node, (nodes.literal_block, nodes.comment)) and 'testnodetype' in node) or \ isinstance(node, nodes.doctest_block) else: def condition(node: Node) -> bool: return isinstance(node, (nodes.literal_block, nodes.comment)) \ and 'testnodetype' in node for node in doctree.findall(condition): # type: Element if self.skipped(node): continue source = node['test'] if 'test' in node else node.astext() filename = self.get_filename_for_node(node, docname) line_number = self.get_line_number(node) if not source: logger.warning(__('no code/output in %s block at %s:%s'), node.get('testnodetype', 'doctest'), filename, line_number) code = TestCode(source, type=node.get('testnodetype', 'doctest'), filename=filename, lineno=line_number, options=node.get('options')) node_groups = node.get('groups', ['default']) if '*' in node_groups: add_to_all_groups.append(code) continue for groupname in node_groups: if groupname not in groups: groups[groupname] = TestGroup(groupname) groups[groupname].add_code(code) for code in add_to_all_groups: for group in groups.values(): group.add_code(code) if self.config.doctest_global_setup: code = TestCode(self.config.doctest_global_setup, 'testsetup', filename=None, lineno=0) for group in groups.values(): group.add_code(code, prepend=True) if self.config.doctest_global_cleanup: code = TestCode(self.config.doctest_global_cleanup, 'testcleanup', filename=None, lineno=0) for group in groups.values(): group.add_code(code) if not groups: return self._out('\nDocument: %s\n----------%s\n' % (docname, '-' * len(docname))) for group in groups.values(): self.test_group(group) # Separately count results from setup code res_f, res_t = self.setup_runner.summarize(self._out, verbose=False) self.setup_failures += res_f self.setup_tries += res_t if self.test_runner.tries: res_f, res_t = self.test_runner.summarize(self._out, verbose=True) self.total_failures += res_f self.total_tries += res_t if self.cleanup_runner.tries: res_f, res_t = self.cleanup_runner.summarize(self._out, verbose=True) self.cleanup_failures += res_f self.cleanup_tries += res_t
def doctree_read(app: Sphinx, doctree: Node) -> None: env = app.builder.env if not hasattr(env, '_viewcode_modules'): env._viewcode_modules = {} # type: ignore def has_tag(modname: str, fullname: str, docname: str, refname: str) -> bool: entry = env._viewcode_modules.get(modname, None) # type: ignore if entry is False: return False code_tags = app.emit_firstresult('viewcode-find-source', modname) if code_tags is None: try: analyzer = ModuleAnalyzer.for_module(modname) analyzer.find_tags() except Exception: env._viewcode_modules[modname] = False # type: ignore return False code = analyzer.code tags = analyzer.tags else: code, tags = code_tags if entry is None or entry[0] != code: entry = code, tags, {}, refname env._viewcode_modules[modname] = entry # type: ignore _, tags, used, _ = entry if fullname in tags: used[fullname] = docname return True return False for objnode in list(doctree.findall(addnodes.desc)): if objnode.get('domain') != 'py': continue names: Set[str] = set() for signode in objnode: if not isinstance(signode, addnodes.desc_signature): continue modname = signode.get('module') fullname = signode.get('fullname') refname = modname if env.config.viewcode_follow_imported_members: new_modname = app.emit_firstresult( 'viewcode-follow-imported', modname, fullname, ) if not new_modname: new_modname = _get_full_modname(app, modname, fullname) modname = new_modname if not modname: continue fullname = signode.get('fullname') if not has_tag(modname, fullname, env.docname, refname): continue if fullname in names: # only one link per name, please continue names.add(fullname) pagename = posixpath.join(OUTPUT_DIRNAME, modname.replace('.', '/')) signode += viewcode_anchor(reftarget=pagename, refid=fullname, refdoc=env.docname)