Merge branch '3.x'

This commit is contained in:
Takeshi KOMIYA
2021-02-13 01:01:30 +09:00
8 changed files with 350 additions and 250 deletions

View File

@@ -104,7 +104,10 @@ Deprecated
* ``sphinx.builders.linkcheck.CheckExternalLinksBuilder.broken`` * ``sphinx.builders.linkcheck.CheckExternalLinksBuilder.broken``
* ``sphinx.builders.linkcheck.CheckExternalLinksBuilder.good`` * ``sphinx.builders.linkcheck.CheckExternalLinksBuilder.good``
* ``sphinx.builders.linkcheck.CheckExternalLinksBuilder.redirected`` * ``sphinx.builders.linkcheck.CheckExternalLinksBuilder.redirected``
* ``sphinx.builders.linkcheck.CheckExternalLinksBuilder.rqueue``
* ``sphinx.builders.linkcheck.CheckExternalLinksBuilder.to_ignore`` * ``sphinx.builders.linkcheck.CheckExternalLinksBuilder.to_ignore``
* ``sphinx.builders.linkcheck.CheckExternalLinksBuilder.workers``
* ``sphinx.builders.linkcheck.CheckExternalLinksBuilder.wqueue``
* ``sphinx.builders.linkcheck.node_line_or_0()`` * ``sphinx.builders.linkcheck.node_line_or_0()``
* ``sphinx.ext.autodoc.AttributeDocumenter.isinstanceattribute()`` * ``sphinx.ext.autodoc.AttributeDocumenter.isinstanceattribute()``
* ``sphinx.ext.autodoc.directive.DocumenterBridge.reporter`` * ``sphinx.ext.autodoc.directive.DocumenterBridge.reporter``
@@ -134,6 +137,7 @@ Features added
* #6550: html: Allow to use HTML permalink texts via * #6550: html: Allow to use HTML permalink texts via
:confval:`html_permalinks_icon` :confval:`html_permalinks_icon`
* #1638: html: Add permalink icons to glossary terms * #1638: html: Add permalink icons to glossary terms
* #8868: html search: performance issue with massive lists
* #8852: i18n: Allow to translate heading syntax in MyST-Parser * #8852: i18n: Allow to translate heading syntax in MyST-Parser
* #8649: imgconverter: Skip availability check if builder supports the image * #8649: imgconverter: Skip availability check if builder supports the image
type type
@@ -221,6 +225,10 @@ Bugs fixed
* #8849: LaTex: code-block printed out of margin (see the opt-in LaTeX syntax * #8849: LaTex: code-block printed out of margin (see the opt-in LaTeX syntax
boolean :ref:`verbatimforcewraps <latexsphinxsetupforcewraps>` for use via boolean :ref:`verbatimforcewraps <latexsphinxsetupforcewraps>` for use via
the :ref:`'sphinxsetup' <latexsphinxsetup>` key of ``latex_elements``) the :ref:`'sphinxsetup' <latexsphinxsetup>` key of ``latex_elements``)
* #8183: LaTeX: Remove substitution_reference nodes from doctree only on LaTeX
builds
* #8865: LaTeX: Restructure the index nodes inside title nodes only on LaTeX
builds
Testing Testing
-------- --------

View File

@@ -24,7 +24,7 @@ class RecipeDirective(ObjectDescription):
def add_target_and_index(self, name_cls, sig, signode): def add_target_and_index(self, name_cls, sig, signode):
signode['ids'].append('recipe' + '-' + sig) signode['ids'].append('recipe' + '-' + sig)
if 'noindex' not in self.options: if 'contains' not in self.options:
ingredients = [ ingredients = [
x.strip() for x in self.options.get('contains').split(',')] x.strip() for x in self.options.get('contains').split(',')]

View File

@@ -97,11 +97,26 @@ The following is a list of deprecated interfaces.
- 5.0 - 5.0
- N/A - N/A
* - ``sphinx.builders.linkcheck.CheckExternalLinksBuilder.rqueue``
- 3.5
- 5.0
- N/A
* - ``sphinx.builders.linkcheck.CheckExternalLinksBuilder.to_ignore`` * - ``sphinx.builders.linkcheck.CheckExternalLinksBuilder.to_ignore``
- 3.5 - 3.5
- 5.0 - 5.0
- N/A - N/A
* - ``sphinx.builders.linkcheck.CheckExternalLinksBuilder.workers``
- 3.5
- 5.0
- N/A
* - ``sphinx.builders.linkcheck.CheckExternalLinksBuilder.wqueue``
- 3.5
- 5.0
- N/A
* - ``sphinx.builders.linkcheck.node_line_or_0()`` * - ``sphinx.builders.linkcheck.node_line_or_0()``
- 3.5 - 3.5
- 5.0 - 5.0

View File

@@ -44,7 +44,7 @@ class SubstitutionDefinitionsRemover(SphinxPostTransform):
default_priority = Substitutions.default_priority + 1 default_priority = Substitutions.default_priority + 1
builders = ('latex',) builders = ('latex',)
def apply(self, **kwargs: Any) -> None: def run(self, **kwargs: Any) -> None:
for node in self.document.traverse(nodes.substitution_definition): for node in self.document.traverse(nodes.substitution_definition):
node.parent.remove(node) node.parent.remove(node)
@@ -192,7 +192,7 @@ class LaTeXFootnoteTransform(SphinxPostTransform):
headings having footnotes headings having footnotes
<footnote_reference> <footnote_reference>
1 1
<footnote ids="1"> <footnote ids="id1">
<label> <label>
1 1
<paragraph> <paragraph>
@@ -203,11 +203,9 @@ class LaTeXFootnoteTransform(SphinxPostTransform):
<section> <section>
<title> <title>
headings having footnotes headings having footnotes
<footnotemark> <footnotemark refid="id1">
1 1
<footnotetext> <footnotetext ids="id1">
footnote body
<footnotetext>
<label> <label>
1 1
<paragraph> <paragraph>
@@ -222,7 +220,7 @@ class LaTeXFootnoteTransform(SphinxPostTransform):
1 1
blah blah blah ... blah blah blah ...
<footnote ids="1"> <footnote ids="id1">
<label> <label>
1 1
<paragraph> <paragraph>
@@ -231,7 +229,7 @@ class LaTeXFootnoteTransform(SphinxPostTransform):
After:: After::
blah blah blah blah blah blah
<footnote ids="1"> <footnote ids="id1">
<label> <label>
1 1
<paragraph> <paragraph>
@@ -251,7 +249,7 @@ class LaTeXFootnoteTransform(SphinxPostTransform):
1 1
blah blah blah ... blah blah blah ...
<footnote ids="1"> <footnote ids="id1">
<label> <label>
1 1
<paragraph> <paragraph>
@@ -260,13 +258,13 @@ class LaTeXFootnoteTransform(SphinxPostTransform):
After:: After::
blah blah blah blah blah blah
<footnote ids="1"> <footnote ids="id1">
<label> <label>
1 1
<paragraph> <paragraph>
footnote body footnote body
blah blah blah blah blah blah
<footnotemark> <footnotemark refid="id1">
1 1
blah blah blah ... blah blah blah ...
@@ -274,7 +272,7 @@ class LaTeXFootnoteTransform(SphinxPostTransform):
Before:: Before::
<footnote ids="1"> <footnote ids="id1">
<label> <label>
1 1
<paragraph> <paragraph>
@@ -291,26 +289,26 @@ class LaTeXFootnoteTransform(SphinxPostTransform):
<table> <table>
<title> <title>
title having footnote_reference title having footnote_reference
<footnote_reference refid="1"> <footnote_reference refid="id1">
1 1
<tgroup> <tgroup>
<thead> <thead>
<row> <row>
<entry> <entry>
header having footnote_reference header having footnote_reference
<footnote_reference refid="2"> <footnote_reference refid="id2">
2 2
<tbody> <tbody>
<row> <row>
... ...
<footnote ids="1"> <footnote ids="id1">
<label> <label>
1 1
<paragraph> <paragraph>
footnote body footnote body
<footnote ids="2"> <footnote ids="id2">
<label> <label>
2 2
<paragraph> <paragraph>
@@ -321,23 +319,23 @@ class LaTeXFootnoteTransform(SphinxPostTransform):
<table> <table>
<title> <title>
title having footnote_reference title having footnote_reference
<footnotemark> <footnotemark refid="id1">
1 1
<tgroup> <tgroup>
<thead> <thead>
<row> <row>
<entry> <entry>
header having footnote_reference header having footnote_reference
<footnotemark> <footnotemark refid="id2">
2 2
<tbody> <tbody>
<footnotetext> <footnotetext ids="id1">
<label> <label>
1 1
<paragraph> <paragraph>
footnote body footnote body
<footnotetext> <footnotetext ids="id2">
<label> <label>
2 2
<paragraph> <paragraph>
@@ -382,7 +380,7 @@ class LaTeXFootnoteVisitor(nodes.NodeVisitor):
self.restricted = None self.restricted = None
pos = node.parent.index(node) pos = node.parent.index(node)
for i, footnote, in enumerate(self.pendings): for i, footnote, in enumerate(self.pendings):
fntext = footnotetext('', *footnote.children) fntext = footnotetext('', *footnote.children, ids=footnote['ids'])
node.parent.insert(pos + i + 1, fntext) node.parent.insert(pos + i + 1, fntext)
self.pendings = [] self.pendings = []
@@ -427,7 +425,7 @@ class LaTeXFootnoteVisitor(nodes.NodeVisitor):
def depart_table(self, node: nodes.table) -> None: def depart_table(self, node: nodes.table) -> None:
tbody = list(node.traverse(nodes.tbody))[0] tbody = list(node.traverse(nodes.tbody))[0]
for footnote in reversed(self.table_footnotes): for footnote in reversed(self.table_footnotes):
fntext = footnotetext('', *footnote.children) fntext = footnotetext('', *footnote.children, ids=footnote['ids'])
tbody.insert(0, fntext) tbody.insert(0, fntext)
self.table_footnotes = [] self.table_footnotes = []
@@ -442,13 +440,13 @@ class LaTeXFootnoteVisitor(nodes.NodeVisitor):
number = node.astext().strip() number = node.astext().strip()
docname = node['docname'] docname = node['docname']
if self.restricted: if self.restricted:
mark = footnotemark('', number) mark = footnotemark('', number, refid=node['refid'])
node.replace_self(mark) node.replace_self(mark)
if (docname, number) not in self.appeared: if (docname, number) not in self.appeared:
footnote = self.get_footnote_by_reference(node) footnote = self.get_footnote_by_reference(node)
self.pendings.append(footnote) self.pendings.append(footnote)
elif (docname, number) in self.appeared: elif (docname, number) in self.appeared:
mark = footnotemark('', number) mark = footnotemark('', number, refid=node['refid'])
node.replace_self(mark) node.replace_self(mark)
else: else:
footnote = self.get_footnote_by_reference(node) footnote = self.get_footnote_by_reference(node)
@@ -574,7 +572,7 @@ class DocumentTargetTransform(SphinxPostTransform):
section['ids'].append(':doc') # special label for :doc: section['ids'].append(':doc') # special label for :doc:
class IndexInSectionTitleTransform(SphinxTransform): class IndexInSectionTitleTransform(SphinxPostTransform):
"""Move index nodes in section title to outside of the title. """Move index nodes in section title to outside of the title.
LaTeX index macro is not compatible with some handling of section titles LaTeX index macro is not compatible with some handling of section titles
@@ -601,8 +599,9 @@ class IndexInSectionTitleTransform(SphinxTransform):
... ...
""" """
default_priority = 400 default_priority = 400
builders = ('latex',)
def apply(self, **kwargs: Any) -> None: def run(self, **kwargs: Any) -> None:
for node in self.document.traverse(nodes.title): for node in self.document.traverse(nodes.title):
if isinstance(node.parent, nodes.section): if isinstance(node.parent, nodes.section):
for i, index in enumerate(node.traverse(addnodes.index)): for i, index in enumerate(node.traverse(addnodes.index)):

View File

@@ -19,7 +19,8 @@ from email.utils import parsedate_to_datetime
from html.parser import HTMLParser from html.parser import HTMLParser
from os import path from os import path
from threading import Thread from threading import Thread
from typing import Any, Dict, List, NamedTuple, Optional, Pattern, Set, Tuple, cast from typing import (Any, Dict, Generator, List, NamedTuple, Optional, Pattern, Set, Tuple,
Union, cast)
from urllib.parse import unquote, urlparse from urllib.parse import unquote, urlparse
from docutils import nodes from docutils import nodes
@@ -29,7 +30,9 @@ from requests.exceptions import HTTPError, TooManyRedirects
from sphinx.application import Sphinx from sphinx.application import Sphinx
from sphinx.builders.dummy import DummyBuilder from sphinx.builders.dummy import DummyBuilder
from sphinx.config import Config
from sphinx.deprecation import RemovedInSphinx50Warning from sphinx.deprecation import RemovedInSphinx50Warning
from sphinx.environment import BuildEnvironment
from sphinx.locale import __ from sphinx.locale import __
from sphinx.transforms.post_transforms import SphinxPostTransform from sphinx.transforms.post_transforms import SphinxPostTransform
from sphinx.util import encode_uri, logging, requests from sphinx.util import encode_uri, logging, requests
@@ -40,10 +43,11 @@ logger = logging.getLogger(__name__)
uri_re = re.compile('([a-z]+:)?//') # matches to foo:// and // (a protocol relative URL) uri_re = re.compile('([a-z]+:)?//') # matches to foo:// and // (a protocol relative URL)
Hyperlink = NamedTuple('Hyperlink', (('next_check', float), Hyperlink = NamedTuple('Hyperlink', (('uri', str),
('uri', Optional[str]), ('docname', str),
('docname', Optional[str]),
('lineno', Optional[int]))) ('lineno', Optional[int])))
CheckRequest = NamedTuple('CheckRequest', (('next_check', float),
('hyperlink', Optional[Hyperlink])))
CheckResult = NamedTuple('CheckResult', (('uri', str), CheckResult = NamedTuple('CheckResult', (('uri', str),
('docname', str), ('docname', str),
('lineno', int), ('lineno', int),
@@ -52,6 +56,9 @@ CheckResult = NamedTuple('CheckResult', (('uri', str),
('code', int))) ('code', int)))
RateLimit = NamedTuple('RateLimit', (('delay', float), ('next_check', float))) RateLimit = NamedTuple('RateLimit', (('delay', float), ('next_check', float)))
# Tuple is old styled CheckRequest
CheckRequestType = Union[CheckRequest, Tuple[float, str, str, int]]
DEFAULT_REQUEST_HEADERS = { DEFAULT_REQUEST_HEADERS = {
'Accept': 'text/html,application/xhtml+xml;q=0.9,*/*;q=0.8', 'Accept': 'text/html,application/xhtml+xml;q=0.9,*/*;q=0.8',
} }
@@ -121,17 +128,8 @@ class CheckExternalLinksBuilder(DummyBuilder):
socket.setdefaulttimeout(5.0) socket.setdefaulttimeout(5.0)
# create queues and worker threads # create queues and worker threads
self.rate_limits = {} # type: Dict[str, RateLimit] self._wqueue = queue.PriorityQueue() # type: queue.PriorityQueue[CheckRequestType]
self.wqueue = queue.PriorityQueue() # type: queue.PriorityQueue self._rqueue = queue.Queue() # type: queue.Queue
self.rqueue = queue.Queue() # type: queue.Queue
self.workers = [] # type: List[Thread]
for i in range(self.config.linkcheck_workers):
thread = HyperlinkAvailabilityCheckWorker(self)
thread.start()
self.workers.append(thread)
def is_ignored_uri(self, uri: str) -> bool:
return any(pat.match(uri) for pat in self.to_ignore)
@property @property
def anchors_ignore(self) -> List[Pattern]: def anchors_ignore(self) -> List[Pattern]:
@@ -202,7 +200,33 @@ class CheckExternalLinksBuilder(DummyBuilder):
RemovedInSphinx50Warning, RemovedInSphinx50Warning,
stacklevel=2, stacklevel=2,
) )
return HyperlinkAvailabilityCheckWorker(self).limit_rate(response) worker = HyperlinkAvailabilityCheckWorker(self.env, self.config,
None, None, {})
return worker.limit_rate(response)
def rqueue(self, response: Response) -> queue.Queue:
warnings.warn(
"%s.%s is deprecated." % (self.__class__.__name__, "rqueue"),
RemovedInSphinx50Warning,
stacklevel=2,
)
return self._rqueue
def workers(self, response: Response) -> List[Thread]:
warnings.warn(
"%s.%s is deprecated." % (self.__class__.__name__, "workers"),
RemovedInSphinx50Warning,
stacklevel=2,
)
return []
def wqueue(self, response: Response) -> queue.Queue:
warnings.warn(
"%s.%s is deprecated." % (self.__class__.__name__, "wqueue"),
RemovedInSphinx50Warning,
stacklevel=2,
)
return self._wqueue
def process_result(self, result: Tuple[str, str, int, str, str, int]) -> None: def process_result(self, result: Tuple[str, str, int, str, str, int]) -> None:
uri, docname, lineno, status, info, code = result uri, docname, lineno, status, info, code = result
@@ -268,53 +292,105 @@ class CheckExternalLinksBuilder(DummyBuilder):
self.json_outfile.write('\n') self.json_outfile.write('\n')
def finish(self) -> None: def finish(self) -> None:
checker = HyperlinkAvailabilityChecker(self.env, self.config, self)
logger.info('') logger.info('')
with open(path.join(self.outdir, 'output.txt'), 'w') as self.txt_outfile,\ with open(path.join(self.outdir, 'output.txt'), 'w') as self.txt_outfile,\
open(path.join(self.outdir, 'output.json'), 'w') as self.json_outfile: open(path.join(self.outdir, 'output.json'), 'w') as self.json_outfile:
total_links = 0 for result in checker.check(self.hyperlinks):
for hyperlink in self.hyperlinks.values(): self.process_result(result)
if self.is_ignored_uri(hyperlink.uri):
self.process_result(
CheckResult(hyperlink.uri, hyperlink.docname, hyperlink.lineno,
'ignored', '', 0))
else:
self.wqueue.put(hyperlink, False)
total_links += 1
done = 0
while done < total_links:
self.process_result(self.rqueue.get())
done += 1
if self._broken: if self._broken:
self.app.statuscode = 1 self.app.statuscode = 1
class HyperlinkAvailabilityChecker:
def __init__(self, env: BuildEnvironment, config: Config,
builder: CheckExternalLinksBuilder = None) -> None:
# Warning: builder argument will be removed in the sphinx-5.0.
# Don't use it from extensions.
# tag: RemovedInSphinx50Warning
self.builder = builder
self.config = config
self.env = env
self.rate_limits = {} # type: Dict[str, RateLimit]
self.workers = [] # type: List[Thread]
self.to_ignore = [re.compile(x) for x in self.config.linkcheck_ignore]
if builder:
self.rqueue = builder._rqueue
self.wqueue = builder._wqueue
else:
self.rqueue = queue.Queue()
self.wqueue = queue.PriorityQueue()
def invoke_threads(self) -> None:
for i in range(self.config.linkcheck_workers):
thread = HyperlinkAvailabilityCheckWorker(self.env, self.config,
self.rqueue, self.wqueue,
self.rate_limits, self.builder)
thread.start()
self.workers.append(thread)
def shutdown_threads(self) -> None:
self.wqueue.join() self.wqueue.join()
# Shutdown threads.
for worker in self.workers: for worker in self.workers:
self.wqueue.put((CHECK_IMMEDIATELY, None, None, None), False) self.wqueue.put(CheckRequest(CHECK_IMMEDIATELY, None), False)
def check(self, hyperlinks: Dict[str, Hyperlink]) -> Generator[CheckResult, None, None]:
self.invoke_threads()
total_links = 0
for hyperlink in hyperlinks.values():
if self.is_ignored_uri(hyperlink.uri):
yield CheckResult(hyperlink.uri, hyperlink.docname, hyperlink.lineno,
'ignored', '', 0)
else:
self.wqueue.put(CheckRequest(CHECK_IMMEDIATELY, hyperlink), False)
total_links += 1
done = 0
while done < total_links:
yield self.rqueue.get()
done += 1
self.shutdown_threads()
def is_ignored_uri(self, uri: str) -> bool:
return any(pat.match(uri) for pat in self.to_ignore)
class HyperlinkAvailabilityCheckWorker(Thread): class HyperlinkAvailabilityCheckWorker(Thread):
"""A worker class for checking the availability of hyperlinks.""" """A worker class for checking the availability of hyperlinks."""
def __init__(self, builder: CheckExternalLinksBuilder) -> None: def __init__(self, env: BuildEnvironment, config: Config, rqueue: queue.Queue,
self.config = builder.config wqueue: queue.Queue, rate_limits: Dict[str, RateLimit],
self.env = builder.env builder: CheckExternalLinksBuilder = None) -> None:
self.rate_limits = builder.rate_limits # Warning: builder argument will be removed in the sphinx-5.0.
self.rqueue = builder.rqueue # Don't use it from extensions.
self.wqueue = builder.wqueue # tag: RemovedInSphinx50Warning
self.config = config
self.env = env
self.rate_limits = rate_limits
self.rqueue = rqueue
self.wqueue = wqueue
self.anchors_ignore = [re.compile(x) self.anchors_ignore = [re.compile(x)
for x in self.config.linkcheck_anchors_ignore] for x in self.config.linkcheck_anchors_ignore]
self.auth = [(re.compile(pattern), auth_info) for pattern, auth_info self.auth = [(re.compile(pattern), auth_info) for pattern, auth_info
in self.config.linkcheck_auth] in self.config.linkcheck_auth]
self.to_ignore = [re.compile(x) for x in self.config.linkcheck_ignore]
self._good = builder._good if builder:
self._broken = builder._broken # if given, fill the result of checks as cache
self._redirected = builder._redirected self._good = builder._good
self._broken = builder._broken
self._redirected = builder._redirected
else:
# only for compatibility. Will be removed in Sphinx-5.0
self._good = set()
self._broken = {}
self._redirected = {}
super().__init__(daemon=True) super().__init__(daemon=True)
@@ -400,7 +476,7 @@ class HyperlinkAvailabilityCheckWorker(Thread):
elif err.response.status_code == 429: elif err.response.status_code == 429:
next_check = self.limit_rate(err.response) next_check = self.limit_rate(err.response)
if next_check is not None: if next_check is not None:
self.wqueue.put((next_check, uri, docname, lineno), False) self.wqueue.put(CheckRequest(next_check, hyperlink), False)
return 'rate-limited', '', 0 return 'rate-limited', '', 0
return 'broken', str(err), 0 return 'broken', str(err), 0
elif err.response.status_code == 503: elif err.response.status_code == 503:
@@ -467,7 +543,17 @@ class HyperlinkAvailabilityCheckWorker(Thread):
return (status, info, code) return (status, info, code)
while True: while True:
next_check, uri, docname, lineno = self.wqueue.get() check_request = self.wqueue.get()
try:
next_check, hyperlink = check_request
if hyperlink is None:
break
uri, docname, lineno = hyperlink
except ValueError:
# old styled check_request (will be deprecated in Sphinx-5.0)
next_check, uri, docname, lineno = check_request
if uri is None: if uri is None:
break break
netloc = urlparse(uri).netloc netloc = urlparse(uri).netloc
@@ -483,7 +569,7 @@ class HyperlinkAvailabilityCheckWorker(Thread):
# Sleep before putting message back in the queue to avoid # Sleep before putting message back in the queue to avoid
# waking up other threads. # waking up other threads.
time.sleep(QUEUE_POLL_SECS) time.sleep(QUEUE_POLL_SECS)
self.wqueue.put((next_check, uri, docname, lineno), False) self.wqueue.put(CheckRequest(next_check, hyperlink), False)
self.wqueue.task_done() self.wqueue.task_done()
continue continue
status, info, code = check(docname) status, info, code = check(docname)
@@ -546,7 +632,7 @@ class HyperlinkCollector(SphinxPostTransform):
continue continue
uri = refnode['refuri'] uri = refnode['refuri']
lineno = get_node_line(refnode) lineno = get_node_line(refnode)
uri_info = Hyperlink(CHECK_IMMEDIATELY, uri, self.env.docname, lineno) uri_info = Hyperlink(uri, self.env.docname, lineno)
if uri not in hyperlinks: if uri not in hyperlinks:
hyperlinks[uri] = uri_info hyperlinks[uri] = uri_info
@@ -555,7 +641,7 @@ class HyperlinkCollector(SphinxPostTransform):
uri = imgnode['candidates'].get('?') uri = imgnode['candidates'].get('?')
if uri and '://' in uri: if uri and '://' in uri:
lineno = get_node_line(imgnode) lineno = get_node_line(imgnode)
uri_info = Hyperlink(CHECK_IMMEDIATELY, uri, self.env.docname, lineno) uri_info = Hyperlink(uri, self.env.docname, lineno)
if uri not in hyperlinks: if uri not in hyperlinks:
hyperlinks[uri] = uri_info hyperlinks[uri] = uri_info

View File

@@ -248,7 +248,7 @@ var Search = {
// results left, load the summary and display it // results left, load the summary and display it
if (results.length) { if (results.length) {
var item = results.pop(); var item = results.pop();
var listItem = $('<li style="display:none"></li>'); var listItem = $('<li></li>');
var requestUrl = ""; var requestUrl = "";
var linkUrl = ""; var linkUrl = "";
if (DOCUMENTATION_OPTIONS.BUILDER === 'dirhtml') { if (DOCUMENTATION_OPTIONS.BUILDER === 'dirhtml') {
@@ -273,9 +273,9 @@ var Search = {
if (item[3]) { if (item[3]) {
listItem.append($('<span> (' + item[3] + ')</span>')); listItem.append($('<span> (' + item[3] + ')</span>'));
Search.output.append(listItem); Search.output.append(listItem);
listItem.slideDown(5, function() { setTimeout(function() {
displayNextItem(); displayNextItem();
}); }, 5);
} else if (DOCUMENTATION_OPTIONS.HAS_SOURCE) { } else if (DOCUMENTATION_OPTIONS.HAS_SOURCE) {
$.ajax({url: requestUrl, $.ajax({url: requestUrl,
dataType: "text", dataType: "text",
@@ -285,16 +285,16 @@ var Search = {
listItem.append(Search.makeSearchSummary(data, searchterms, hlterms)); listItem.append(Search.makeSearchSummary(data, searchterms, hlterms));
} }
Search.output.append(listItem); Search.output.append(listItem);
listItem.slideDown(5, function() { setTimeout(function() {
displayNextItem(); displayNextItem();
}); }, 5);
}}); }});
} else { } else {
// no source available, just display title // no source available, just display title
Search.output.append(listItem); Search.output.append(listItem);
listItem.slideDown(5, function() { setTimeout(function() {
displayNextItem(); displayNextItem();
}); }, 5);
} }
} }
// search finished, update title and status message // search finished, update title and status message

View File

@@ -57,6 +57,8 @@ ENUMERATE_LIST_STYLE = defaultdict(lambda: r'\arabic',
'upperroman': r'\Roman', 'upperroman': r'\Roman',
}) })
CR = '\n'
BLANKLINE = '\n\n'
EXTRA_RE = re.compile(r'^(.*\S)\s+\(([^()]*)\)\s*$') EXTRA_RE = re.compile(r'^(.*\S)\s+\(([^()]*)\)\s*$')
@@ -163,16 +165,16 @@ class Table:
elif self.colwidths and 'colwidths-given' in self.classes: elif self.colwidths and 'colwidths-given' in self.classes:
total = sum(self.colwidths) total = sum(self.colwidths)
colspecs = ['\\X{%d}{%d}' % (width, total) for width in self.colwidths] colspecs = ['\\X{%d}{%d}' % (width, total) for width in self.colwidths]
return '{|%s|}\n' % '|'.join(colspecs) return '{|%s|}' % '|'.join(colspecs) + CR
elif self.has_problematic: elif self.has_problematic:
return '{|*{%d}{\\X{1}{%d}|}}\n' % (self.colcount, self.colcount) return '{|*{%d}{\\X{1}{%d}|}}' % (self.colcount, self.colcount) + CR
elif self.get_table_type() == 'tabulary': elif self.get_table_type() == 'tabulary':
# sphinx.sty sets T to be J by default. # sphinx.sty sets T to be J by default.
return '{|' + ('T|' * self.colcount) + '}\n' return '{|' + ('T|' * self.colcount) + '}' + CR
elif self.has_oldproblematic: elif self.has_oldproblematic:
return '{|*{%d}{\\X{1}{%d}|}}\n' % (self.colcount, self.colcount) return '{|*{%d}{\\X{1}{%d}|}}' % (self.colcount, self.colcount) + CR
else: else:
return '{|' + ('l|' * self.colcount) + '}\n' return '{|' + ('l|' * self.colcount) + '}' + CR
def add_cell(self, height: int, width: int) -> None: def add_cell(self, height: int, width: int) -> None:
"""Adds a new cell to a table. """Adds a new cell to a table.
@@ -371,8 +373,8 @@ class LaTeXTranslator(SphinxTranslator):
if (self.config.language not in {None, 'en', 'ja'} and if (self.config.language not in {None, 'en', 'ja'} and
'fncychap' not in self.config.latex_elements): 'fncychap' not in self.config.latex_elements):
# use Sonny style if any language specified (except English) # use Sonny style if any language specified (except English)
self.elements['fncychap'] = ('\\usepackage[Sonny]{fncychap}\n' self.elements['fncychap'] = ('\\usepackage[Sonny]{fncychap}' + CR +
'\\ChNameVar{\\Large\\normalfont\\sffamily}\n' '\\ChNameVar{\\Large\\normalfont\\sffamily}' + CR +
'\\ChTitleVar{\\Large\\normalfont\\sffamily}') '\\ChTitleVar{\\Large\\normalfont\\sffamily}')
self.babel = self.builder.babel self.babel = self.builder.babel
@@ -496,16 +498,16 @@ class LaTeXTranslator(SphinxTranslator):
prefix = '' prefix = ''
suffix = '' suffix = ''
return ('%s\\renewcommand{%s}{%s}%s\n' % (prefix, command, definition, suffix)) return '%s\\renewcommand{%s}{%s}%s' % (prefix, command, definition, suffix) + CR
def generate_indices(self) -> str: def generate_indices(self) -> str:
def generate(content: List[Tuple[str, List[IndexEntry]]], collapsed: bool) -> None: def generate(content: List[Tuple[str, List[IndexEntry]]], collapsed: bool) -> None:
ret.append('\\begin{sphinxtheindex}\n') ret.append('\\begin{sphinxtheindex}' + CR)
ret.append('\\let\\bigletter\\sphinxstyleindexlettergroup\n') ret.append('\\let\\bigletter\\sphinxstyleindexlettergroup' + CR)
for i, (letter, entries) in enumerate(content): for i, (letter, entries) in enumerate(content):
if i > 0: if i > 0:
ret.append('\\indexspace\n') ret.append('\\indexspace' + CR)
ret.append('\\bigletter{%s}\n' % self.escape(letter)) ret.append('\\bigletter{%s}' % self.escape(letter) + CR)
for entry in entries: for entry in entries:
if not entry[3]: if not entry[3]:
continue continue
@@ -514,9 +516,9 @@ class LaTeXTranslator(SphinxTranslator):
if entry[4]: if entry[4]:
# add "extra" info # add "extra" info
ret.append('\\sphinxstyleindexextra{%s}' % self.encode(entry[4])) ret.append('\\sphinxstyleindexextra{%s}' % self.encode(entry[4]))
ret.append('\\sphinxstyleindexpageref{%s:%s}\n' % ret.append('\\sphinxstyleindexpageref{%s:%s}' %
(entry[2], self.idescape(entry[3]))) (entry[2], self.idescape(entry[3])) + CR)
ret.append('\\end{sphinxtheindex}\n') ret.append('\\end{sphinxtheindex}' + CR)
ret = [] ret = []
# latex_domain_indices can be False/True or a list of index names # latex_domain_indices can be False/True or a list of index names
@@ -532,8 +534,7 @@ class LaTeXTranslator(SphinxTranslator):
self.builder.docnames) self.builder.docnames)
if not content: if not content:
continue continue
ret.append('\\renewcommand{\\indexname}{%s}\n' % ret.append('\\renewcommand{\\indexname}{%s}' % indexcls.localname + CR)
indexcls.localname)
generate(content, collapsed) generate(content, collapsed)
return ''.join(ret) return ''.join(ret)
@@ -563,7 +564,7 @@ class LaTeXTranslator(SphinxTranslator):
self.first_document = 0 self.first_document = 0
elif self.first_document == 0: elif self.first_document == 0:
# ... and all others are the appendices # ... and all others are the appendices
self.body.append('\n\\appendix\n') self.body.append(CR + '\\appendix' + CR)
self.first_document = -1 self.first_document = -1
if 'docname' in node: if 'docname' in node:
self.body.append(self.hypertarget(':doc')) self.body.append(self.hypertarget(':doc'))
@@ -582,7 +583,7 @@ class LaTeXTranslator(SphinxTranslator):
def visit_section(self, node: Element) -> None: def visit_section(self, node: Element) -> None:
if not self.this_is_the_title: if not self.this_is_the_title:
self.sectionlevel += 1 self.sectionlevel += 1
self.body.append('\n\n') self.body.append(BLANKLINE)
def depart_section(self, node: Element) -> None: def depart_section(self, node: Element) -> None:
self.sectionlevel = max(self.sectionlevel - 1, self.sectionlevel = max(self.sectionlevel - 1,
@@ -596,11 +597,11 @@ class LaTeXTranslator(SphinxTranslator):
def visit_topic(self, node: Element) -> None: def visit_topic(self, node: Element) -> None:
self.in_minipage = 1 self.in_minipage = 1
self.body.append('\n\\begin{sphinxShadowBox}\n') self.body.append(CR + '\\begin{sphinxShadowBox}' + CR)
def depart_topic(self, node: Element) -> None: def depart_topic(self, node: Element) -> None:
self.in_minipage = 0 self.in_minipage = 0
self.body.append('\\end{sphinxShadowBox}\n') self.body.append('\\end{sphinxShadowBox}' + CR)
visit_sidebar = visit_topic visit_sidebar = visit_topic
depart_sidebar = depart_topic depart_sidebar = depart_topic
@@ -611,12 +612,12 @@ class LaTeXTranslator(SphinxTranslator):
pass pass
def visit_productionlist(self, node: Element) -> None: def visit_productionlist(self, node: Element) -> None:
self.body.append('\n\n') self.body.append(BLANKLINE)
self.body.append('\\begin{productionlist}\n') self.body.append('\\begin{productionlist}' + CR)
self.in_production_list = 1 self.in_production_list = 1
def depart_productionlist(self, node: Element) -> None: def depart_productionlist(self, node: Element) -> None:
self.body.append('\\end{productionlist}\n\n') self.body.append('\\end{productionlist}' + BLANKLINE)
self.in_production_list = 0 self.in_production_list = 0
def visit_production(self, node: Element) -> None: def visit_production(self, node: Element) -> None:
@@ -628,7 +629,7 @@ class LaTeXTranslator(SphinxTranslator):
self.body.append('\\productioncont{') self.body.append('\\productioncont{')
def depart_production(self, node: Element) -> None: def depart_production(self, node: Element) -> None:
self.body.append('}\n') self.body.append('}' + CR)
def visit_transition(self, node: Element) -> None: def visit_transition(self, node: Element) -> None:
self.body.append(self.elements['transition']) self.body.append(self.elements['transition'])
@@ -663,16 +664,16 @@ class LaTeXTranslator(SphinxTranslator):
except IndexError: except IndexError:
# just use "subparagraph", it's not numbered anyway # just use "subparagraph", it's not numbered anyway
self.body.append(r'\%s%s{' % (self.sectionnames[-1], short)) self.body.append(r'\%s%s{' % (self.sectionnames[-1], short))
self.context.append('}\n' + self.hypertarget_to(node.parent)) self.context.append('}' + CR + self.hypertarget_to(node.parent))
elif isinstance(parent, nodes.topic): elif isinstance(parent, nodes.topic):
self.body.append(r'\sphinxstyletopictitle{') self.body.append(r'\sphinxstyletopictitle{')
self.context.append('}\n') self.context.append('}' + CR)
elif isinstance(parent, nodes.sidebar): elif isinstance(parent, nodes.sidebar):
self.body.append(r'\sphinxstylesidebartitle{') self.body.append(r'\sphinxstylesidebartitle{')
self.context.append('}\n') self.context.append('}' + CR)
elif isinstance(parent, nodes.Admonition): elif isinstance(parent, nodes.Admonition):
self.body.append('{') self.body.append('{')
self.context.append('}\n') self.context.append('}' + CR)
elif isinstance(parent, nodes.table): elif isinstance(parent, nodes.table):
# Redirect body output until title is finished. # Redirect body output until title is finished.
self.pushbody([]) self.pushbody([])
@@ -681,7 +682,7 @@ class LaTeXTranslator(SphinxTranslator):
'admonition or sidebar'), 'admonition or sidebar'),
location=node) location=node)
self.body.append('\\sphinxstyleothertitle{') self.body.append('\\sphinxstyleothertitle{')
self.context.append('}\n') self.context.append('}' + CR)
self.in_title = 1 self.in_title = 1
def depart_title(self, node: Element) -> None: def depart_title(self, node: Element) -> None:
@@ -694,7 +695,7 @@ class LaTeXTranslator(SphinxTranslator):
def visit_subtitle(self, node: Element) -> None: def visit_subtitle(self, node: Element) -> None:
if isinstance(node.parent, nodes.sidebar): if isinstance(node.parent, nodes.sidebar):
self.body.append('\\sphinxstylesidebarsubtitle{') self.body.append('\\sphinxstylesidebarsubtitle{')
self.context.append('}\n') self.context.append('}' + CR)
else: else:
self.context.append('') self.context.append('')
@@ -703,19 +704,19 @@ class LaTeXTranslator(SphinxTranslator):
def visit_desc(self, node: Element) -> None: def visit_desc(self, node: Element) -> None:
if self.config.latex_show_urls == 'footnote': if self.config.latex_show_urls == 'footnote':
self.body.append('\n\n') self.body.append(BLANKLINE)
self.body.append('\\begin{savenotes}\\begin{fulllineitems}\n') self.body.append('\\begin{savenotes}\\begin{fulllineitems}' + CR)
else: else:
self.body.append('\n\n') self.body.append(BLANKLINE)
self.body.append('\\begin{fulllineitems}\n') self.body.append('\\begin{fulllineitems}' + CR)
if self.table: if self.table:
self.table.has_problematic = True self.table.has_problematic = True
def depart_desc(self, node: Element) -> None: def depart_desc(self, node: Element) -> None:
if self.config.latex_show_urls == 'footnote': if self.config.latex_show_urls == 'footnote':
self.body.append('\n\\end{fulllineitems}\\end{savenotes}\n\n') self.body.append(CR + '\\end{fulllineitems}\\end{savenotes}' + BLANKLINE)
else: else:
self.body.append('\n\\end{fulllineitems}\n\n') self.body.append(CR + '\\end{fulllineitems}' + BLANKLINE)
def _visit_signature_line(self, node: Element) -> None: def _visit_signature_line(self, node: Element) -> None:
for child in node: for child in node:
@@ -737,14 +738,14 @@ class LaTeXTranslator(SphinxTranslator):
if not node.get('is_multiline'): if not node.get('is_multiline'):
self._visit_signature_line(node) self._visit_signature_line(node)
else: else:
self.body.append('%\n') self.body.append('%' + CR)
self.body.append('\\pysigstartmultiline\n') self.body.append('\\pysigstartmultiline' + CR)
def depart_desc_signature(self, node: Element) -> None: def depart_desc_signature(self, node: Element) -> None:
if not node.get('is_multiline'): if not node.get('is_multiline'):
self._depart_signature_line(node) self._depart_signature_line(node)
else: else:
self.body.append('%\n') self.body.append('%' + CR)
self.body.append('\\pysigstopmultiline') self.body.append('\\pysigstopmultiline')
def visit_desc_signature_line(self, node: Element) -> None: def visit_desc_signature_line(self, node: Element) -> None:
@@ -823,18 +824,18 @@ class LaTeXTranslator(SphinxTranslator):
pass pass
def visit_seealso(self, node: Element) -> None: def visit_seealso(self, node: Element) -> None:
self.body.append('\n\n') self.body.append(BLANKLINE)
self.body.append('\\sphinxstrong{%s:}\n' % admonitionlabels['seealso']) self.body.append('\\sphinxstrong{%s:}' % admonitionlabels['seealso'] + CR)
self.body.append('\\nopagebreak\n\n') self.body.append('\\nopagebreak' + BLANKLINE)
def depart_seealso(self, node: Element) -> None: def depart_seealso(self, node: Element) -> None:
self.body.append("\n\n") self.body.append(BLANKLINE)
def visit_rubric(self, node: Element) -> None: def visit_rubric(self, node: Element) -> None:
if len(node) == 1 and node.astext() in ('Footnotes', _('Footnotes')): if len(node) == 1 and node.astext() in ('Footnotes', _('Footnotes')):
raise nodes.SkipNode raise nodes.SkipNode
self.body.append('\\subsubsection*{') self.body.append('\\subsubsection*{')
self.context.append('}\n') self.context.append('}' + CR)
self.in_title = 1 self.in_title = 1
def depart_rubric(self, node: Element) -> None: def depart_rubric(self, node: Element) -> None:
@@ -849,18 +850,18 @@ class LaTeXTranslator(SphinxTranslator):
if self.in_parsed_literal: if self.in_parsed_literal:
self.body.append('\\begin{footnote}[%s]' % label.astext()) self.body.append('\\begin{footnote}[%s]' % label.astext())
else: else:
self.body.append('%\n') self.body.append('%' + CR)
self.body.append('\\begin{footnote}[%s]' % label.astext()) self.body.append('\\begin{footnote}[%s]' % label.astext())
if 'auto' not in node: if 'auto' not in node:
self.body.append('\\phantomsection' self.body.append('\\phantomsection'
'\\label{\\thesphinxscope.%s}%%\n' % label.astext()) '\\label{\\thesphinxscope.%s}%%' % label.astext() + CR)
self.body.append('\\sphinxAtStartFootnote\n') self.body.append('\\sphinxAtStartFootnote' + CR)
def depart_footnote(self, node: Element) -> None: def depart_footnote(self, node: Element) -> None:
if self.in_parsed_literal: if self.in_parsed_literal:
self.body.append('\\end{footnote}') self.body.append('\\end{footnote}')
else: else:
self.body.append('%\n') self.body.append('%' + CR)
self.body.append('\\end{footnote}') self.body.append('\\end{footnote}')
self.in_footnote -= 1 self.in_footnote -= 1
@@ -888,7 +889,7 @@ class LaTeXTranslator(SphinxTranslator):
self.tables.append(Table(node)) self.tables.append(Table(node))
if self.next_table_colspec: if self.next_table_colspec:
self.table.colspec = '{%s}\n' % self.next_table_colspec self.table.colspec = '{%s}' % self.next_table_colspec + CR
if 'colwidths-given' in node.get('classes', []): if 'colwidths-given' in node.get('classes', []):
logger.info(__('both tabularcolumns and :widths: option are given. ' logger.info(__('both tabularcolumns and :widths: option are given. '
':widths: is ignored.'), location=node) ':widths: is ignored.'), location=node)
@@ -899,9 +900,9 @@ class LaTeXTranslator(SphinxTranslator):
table_type = self.table.get_table_type() table_type = self.table.get_table_type()
table = self.render(table_type + '.tex_t', table = self.render(table_type + '.tex_t',
dict(table=self.table, labels=labels)) dict(table=self.table, labels=labels))
self.body.append("\n\n") self.body.append(BLANKLINE)
self.body.append(table) self.body.append(table)
self.body.append("\n") self.body.append(CR)
self.tables.pop() self.tables.pop()
@@ -956,7 +957,7 @@ class LaTeXTranslator(SphinxTranslator):
(cell.width, cell.cell_id)) (cell.width, cell.cell_id))
def depart_row(self, node: Element) -> None: def depart_row(self, node: Element) -> None:
self.body.append('\\\\\n') self.body.append('\\\\' + CR)
cells = [self.table.cell(self.table.row, i) for i in range(self.table.colcount)] cells = [self.table.cell(self.table.row, i) for i in range(self.table.colcount)]
underlined = [cell.row + cell.height == self.table.row + 1 for cell in cells] underlined = [cell.row + cell.height == self.table.row + 1 for cell in cells]
if all(underlined): if all(underlined):
@@ -981,22 +982,22 @@ class LaTeXTranslator(SphinxTranslator):
if cell.width > 1: if cell.width > 1:
if self.config.latex_use_latex_multicolumn: if self.config.latex_use_latex_multicolumn:
if self.table.col == 0: if self.table.col == 0:
self.body.append('\\multicolumn{%d}{|l|}{%%\n' % cell.width) self.body.append('\\multicolumn{%d}{|l|}{%%' % cell.width + CR)
else: else:
self.body.append('\\multicolumn{%d}{l|}{%%\n' % cell.width) self.body.append('\\multicolumn{%d}{l|}{%%' % cell.width + CR)
context = '}%\n' context = '}%' + CR
else: else:
self.body.append('\\sphinxstartmulticolumn{%d}%%\n' % cell.width) self.body.append('\\sphinxstartmulticolumn{%d}%%' % cell.width + CR)
context = '\\sphinxstopmulticolumn\n' context = '\\sphinxstopmulticolumn' + CR
if cell.height > 1: if cell.height > 1:
# \sphinxmultirow 2nd arg "cell_id" will serve as id for LaTeX macros as well # \sphinxmultirow 2nd arg "cell_id" will serve as id for LaTeX macros as well
self.body.append('\\sphinxmultirow{%d}{%d}{%%\n' % (cell.height, cell.cell_id)) self.body.append('\\sphinxmultirow{%d}{%d}{%%' % (cell.height, cell.cell_id) + CR)
context = '}%\n' + context context = '}%' + CR + context
if cell.width > 1 or cell.height > 1: if cell.width > 1 or cell.height > 1:
self.body.append('\\begin{varwidth}[t]{\\sphinxcolwidth{%d}{%d}}\n' self.body.append('\\begin{varwidth}[t]{\\sphinxcolwidth{%d}{%d}}'
% (cell.width, self.table.colcount)) % (cell.width, self.table.colcount) + CR)
context = ('\\par\n\\vskip-\\baselineskip' context = ('\\par' + CR + '\\vskip-\\baselineskip'
'\\vbox{\\hbox{\\strut}}\\end{varwidth}%\n') + context '\\vbox{\\hbox{\\strut}}\\end{varwidth}%' + CR + context)
self.needs_linetrimming = 1 self.needs_linetrimming = 1
if len(node.traverse(nodes.paragraph)) >= 2: if len(node.traverse(nodes.paragraph)) >= 2:
self.table.has_oldproblematic = True self.table.has_oldproblematic = True
@@ -1015,7 +1016,7 @@ class LaTeXTranslator(SphinxTranslator):
body = self.popbody() body = self.popbody()
# Remove empty lines from top of merged cell # Remove empty lines from top of merged cell
while body and body[0] == "\n": while body and body[0] == CR:
body.pop(0) body.pop(0)
self.body.extend(body) self.body.extend(body)
@@ -1047,20 +1048,20 @@ class LaTeXTranslator(SphinxTranslator):
# comma-separated list here # comma-separated list here
bullet_list = cast(nodes.bullet_list, node[0]) bullet_list = cast(nodes.bullet_list, node[0])
list_items = cast(Iterable[nodes.list_item], bullet_list) list_items = cast(Iterable[nodes.list_item], bullet_list)
self.body.append('\n\n') self.body.append(BLANKLINE)
self.body.append(', '.join(n.astext() for n in list_items) + '.') self.body.append(', '.join(n.astext() for n in list_items) + '.')
self.body.append('\n\n') self.body.append(BLANKLINE)
raise nodes.SkipNode raise nodes.SkipNode
def visit_bullet_list(self, node: Element) -> None: def visit_bullet_list(self, node: Element) -> None:
if not self.compact_list: if not self.compact_list:
self.body.append('\\begin{itemize}\n') self.body.append('\\begin{itemize}' + CR)
if self.table: if self.table:
self.table.has_problematic = True self.table.has_problematic = True
def depart_bullet_list(self, node: Element) -> None: def depart_bullet_list(self, node: Element) -> None:
if not self.compact_list: if not self.compact_list:
self.body.append('\\end{itemize}\n') self.body.append('\\end{itemize}' + CR)
def visit_enumerated_list(self, node: Element) -> None: def visit_enumerated_list(self, node: Element) -> None:
def get_enumtype(node: Element) -> str: def get_enumtype(node: Element) -> str:
@@ -1085,16 +1086,16 @@ class LaTeXTranslator(SphinxTranslator):
prefix = node.get('prefix', '') prefix = node.get('prefix', '')
suffix = node.get('suffix', '.') suffix = node.get('suffix', '.')
self.body.append('\\begin{enumerate}\n') self.body.append('\\begin{enumerate}' + CR)
self.body.append('\\sphinxsetlistlabels{%s}{%s}{%s}{%s}{%s}%%\n' % self.body.append('\\sphinxsetlistlabels{%s}{%s}{%s}{%s}{%s}%%' %
(style, enum, enumnext, prefix, suffix)) (style, enum, enumnext, prefix, suffix) + CR)
if 'start' in node: if 'start' in node:
self.body.append('\\setcounter{%s}{%d}\n' % (enum, node['start'] - 1)) self.body.append('\\setcounter{%s}{%d}' % (enum, node['start'] - 1) + CR)
if self.table: if self.table:
self.table.has_problematic = True self.table.has_problematic = True
def depart_enumerated_list(self, node: Element) -> None: def depart_enumerated_list(self, node: Element) -> None:
self.body.append('\\end{enumerate}\n') self.body.append('\\end{enumerate}' + CR)
def visit_list_item(self, node: Element) -> None: def visit_list_item(self, node: Element) -> None:
# Append "{}" in case the next character is "[", which would break # Append "{}" in case the next character is "[", which would break
@@ -1102,15 +1103,15 @@ class LaTeXTranslator(SphinxTranslator):
self.body.append(r'\item {} ') self.body.append(r'\item {} ')
def depart_list_item(self, node: Element) -> None: def depart_list_item(self, node: Element) -> None:
self.body.append('\n') self.body.append(CR)
def visit_definition_list(self, node: Element) -> None: def visit_definition_list(self, node: Element) -> None:
self.body.append('\\begin{description}\n') self.body.append('\\begin{description}' + CR)
if self.table: if self.table:
self.table.has_problematic = True self.table.has_problematic = True
def depart_definition_list(self, node: Element) -> None: def depart_definition_list(self, node: Element) -> None:
self.body.append('\\end{description}\n') self.body.append('\\end{description}' + CR)
def visit_definition_list_item(self, node: Element) -> None: def visit_definition_list_item(self, node: Element) -> None:
pass pass
@@ -1143,15 +1144,15 @@ class LaTeXTranslator(SphinxTranslator):
pass pass
def depart_definition(self, node: Element) -> None: def depart_definition(self, node: Element) -> None:
self.body.append('\n') self.body.append(CR)
def visit_field_list(self, node: Element) -> None: def visit_field_list(self, node: Element) -> None:
self.body.append('\\begin{quote}\\begin{description}\n') self.body.append('\\begin{quote}\\begin{description}' + CR)
if self.table: if self.table:
self.table.has_problematic = True self.table.has_problematic = True
def depart_field_list(self, node: Element) -> None: def depart_field_list(self, node: Element) -> None:
self.body.append('\\end{description}\\end{quote}\n') self.body.append('\\end{description}\\end{quote}' + CR)
def visit_field(self, node: Element) -> None: def visit_field(self, node: Element) -> None:
pass pass
@@ -1171,7 +1172,7 @@ class LaTeXTranslator(SphinxTranslator):
not isinstance(node.parent[index - 1], nodes.paragraph) and not isinstance(node.parent[index - 1], nodes.paragraph) and
not isinstance(node.parent[index - 1], nodes.compound)): not isinstance(node.parent[index - 1], nodes.compound)):
# insert blank line, if the paragraph follows a non-paragraph node in a compound # insert blank line, if the paragraph follows a non-paragraph node in a compound
self.body.append('\\noindent\n') self.body.append('\\noindent' + CR)
elif index == 1 and isinstance(node.parent, (nodes.footnote, footnotetext)): elif index == 1 and isinstance(node.parent, (nodes.footnote, footnotetext)):
# don't insert blank line, if the paragraph is second child of a footnote # don't insert blank line, if the paragraph is second child of a footnote
# (first one is label node) # (first one is label node)
@@ -1180,33 +1181,33 @@ class LaTeXTranslator(SphinxTranslator):
# the \sphinxAtStartPar is to allow hyphenation of first word of # the \sphinxAtStartPar is to allow hyphenation of first word of
# a paragraph in narrow contexts such as in a table cell # a paragraph in narrow contexts such as in a table cell
# added as two items (cf. line trimming in depart_entry()) # added as two items (cf. line trimming in depart_entry())
self.body.extend(['\n', '\\sphinxAtStartPar\n']) self.body.extend([CR, '\\sphinxAtStartPar' + CR])
def depart_paragraph(self, node: Element) -> None: def depart_paragraph(self, node: Element) -> None:
self.body.append('\n') self.body.append(CR)
def visit_centered(self, node: Element) -> None: def visit_centered(self, node: Element) -> None:
self.body.append('\n\\begin{center}') self.body.append(CR + '\\begin{center}')
if self.table: if self.table:
self.table.has_problematic = True self.table.has_problematic = True
def depart_centered(self, node: Element) -> None: def depart_centered(self, node: Element) -> None:
self.body.append('\n\\end{center}') self.body.append(CR + '\\end{center}')
def visit_hlist(self, node: Element) -> None: def visit_hlist(self, node: Element) -> None:
self.compact_list += 1 self.compact_list += 1
ncolumns = node['ncolumns'] ncolumns = node['ncolumns']
if self.compact_list > 1: if self.compact_list > 1:
self.body.append('\\setlength{\\multicolsep}{0pt}\n') self.body.append('\\setlength{\\multicolsep}{0pt}' + CR)
self.body.append('\\begin{multicols}{' + ncolumns + '}\\raggedright\n') self.body.append('\\begin{multicols}{' + ncolumns + '}\\raggedright' + CR)
self.body.append('\\begin{itemize}\\setlength{\\itemsep}{0pt}' self.body.append('\\begin{itemize}\\setlength{\\itemsep}{0pt}'
'\\setlength{\\parskip}{0pt}\n') '\\setlength{\\parskip}{0pt}' + CR)
if self.table: if self.table:
self.table.has_problematic = True self.table.has_problematic = True
def depart_hlist(self, node: Element) -> None: def depart_hlist(self, node: Element) -> None:
self.compact_list -= 1 self.compact_list -= 1
self.body.append('\\end{itemize}\\raggedcolumns\\end{multicols}\n') self.body.append('\\end{itemize}\\raggedcolumns\\end{multicols}' + CR)
def visit_hlistcol(self, node: Element) -> None: def visit_hlistcol(self, node: Element) -> None:
pass pass
@@ -1282,8 +1283,8 @@ class LaTeXTranslator(SphinxTranslator):
pre.append('{\\sphinxunactivateextrasandspace ') pre.append('{\\sphinxunactivateextrasandspace ')
post.append('}') post.append('}')
if not is_inline and not has_hyperlink: if not is_inline and not has_hyperlink:
pre.append('\n\\noindent') pre.append(CR + '\\noindent')
post.append('\n') post.append(CR)
pre.reverse() pre.reverse()
if node['uri'] in self.builder.images: if node['uri'] in self.builder.images:
uri = self.builder.images[node['uri']] uri = self.builder.images[node['uri']]
@@ -1322,35 +1323,35 @@ class LaTeXTranslator(SphinxTranslator):
if 'width' in node: if 'width' in node:
length = self.latex_image_length(node['width']) length = self.latex_image_length(node['width'])
if length: if length:
self.body.append('\\begin{sphinxfigure-in-table}[%s]\n' % length) self.body.append('\\begin{sphinxfigure-in-table}[%s]' % length + CR)
self.body.append('\\centering\n') self.body.append('\\centering' + CR)
else: else:
self.body.append('\\begin{sphinxfigure-in-table}\n') self.body.append('\\begin{sphinxfigure-in-table}' + CR)
self.body.append('\\centering\n') self.body.append('\\centering' + CR)
if any(isinstance(child, nodes.caption) for child in node): if any(isinstance(child, nodes.caption) for child in node):
self.body.append('\\capstart') self.body.append('\\capstart')
self.context.append('\\end{sphinxfigure-in-table}\\relax\n') self.context.append('\\end{sphinxfigure-in-table}\\relax' + CR)
elif node.get('align', '') in ('left', 'right'): elif node.get('align', '') in ('left', 'right'):
length = None length = None
if 'width' in node: if 'width' in node:
length = self.latex_image_length(node['width']) length = self.latex_image_length(node['width'])
elif isinstance(node[0], nodes.image) and 'width' in node[0]: elif isinstance(node[0], nodes.image) and 'width' in node[0]:
length = self.latex_image_length(node[0]['width']) length = self.latex_image_length(node[0]['width'])
self.body.append('\n\n') # Insert a blank line to prevent infinite loop self.body.append(BLANKLINE) # Insert a blank line to prevent infinite loop
# https://github.com/sphinx-doc/sphinx/issues/7059 # https://github.com/sphinx-doc/sphinx/issues/7059
self.body.append('\\begin{wrapfigure}{%s}{%s}\n' % self.body.append('\\begin{wrapfigure}{%s}{%s}' %
('r' if node['align'] == 'right' else 'l', length or '0pt')) ('r' if node['align'] == 'right' else 'l', length or '0pt') + CR)
self.body.append('\\centering') self.body.append('\\centering')
self.context.append('\\end{wrapfigure}\n') self.context.append('\\end{wrapfigure}' + CR)
elif self.in_minipage: elif self.in_minipage:
self.body.append('\n\\begin{center}') self.body.append(CR + '\\begin{center}')
self.context.append('\\end{center}\n') self.context.append('\\end{center}' + CR)
else: else:
self.body.append('\n\\begin{figure}[%s]\n' % align) self.body.append(CR + '\\begin{figure}[%s]' % align + CR)
self.body.append('\\centering\n') self.body.append('\\centering' + CR)
if any(isinstance(child, nodes.caption) for child in node): if any(isinstance(child, nodes.caption) for child in node):
self.body.append('\\capstart\n') self.body.append('\\capstart' + CR)
self.context.append('\\end{figure}\n') self.context.append('\\end{figure}' + CR)
def depart_figure(self, node: Element) -> None: def depart_figure(self, node: Element) -> None:
self.body.append(self.context.pop()) self.body.append(self.context.pop())
@@ -1374,27 +1375,27 @@ class LaTeXTranslator(SphinxTranslator):
self.in_caption -= 1 self.in_caption -= 1
def visit_legend(self, node: Element) -> None: def visit_legend(self, node: Element) -> None:
self.body.append('\n\\begin{sphinxlegend}') self.body.append(CR + '\\begin{sphinxlegend}')
def depart_legend(self, node: Element) -> None: def depart_legend(self, node: Element) -> None:
self.body.append('\\end{sphinxlegend}\n') self.body.append('\\end{sphinxlegend}' + CR)
def visit_admonition(self, node: Element) -> None: def visit_admonition(self, node: Element) -> None:
self.body.append('\n\\begin{sphinxadmonition}{note}') self.body.append(CR + '\\begin{sphinxadmonition}{note}')
self.no_latex_floats += 1 self.no_latex_floats += 1
def depart_admonition(self, node: Element) -> None: def depart_admonition(self, node: Element) -> None:
self.body.append('\\end{sphinxadmonition}\n') self.body.append('\\end{sphinxadmonition}' + CR)
self.no_latex_floats -= 1 self.no_latex_floats -= 1
def _visit_named_admonition(self, node: Element) -> None: def _visit_named_admonition(self, node: Element) -> None:
label = admonitionlabels[node.tagname] label = admonitionlabels[node.tagname]
self.body.append('\n\\begin{sphinxadmonition}{%s}{%s:}' % self.body.append(CR + '\\begin{sphinxadmonition}{%s}{%s:}' %
(node.tagname, label)) (node.tagname, label))
self.no_latex_floats += 1 self.no_latex_floats += 1
def _depart_named_admonition(self, node: Element) -> None: def _depart_named_admonition(self, node: Element) -> None:
self.body.append('\\end{sphinxadmonition}\n') self.body.append('\\end{sphinxadmonition}' + CR)
self.no_latex_floats -= 1 self.no_latex_floats -= 1
visit_attention = _visit_named_admonition visit_attention = _visit_named_admonition
@@ -1437,7 +1438,7 @@ class LaTeXTranslator(SphinxTranslator):
# insert blank line, if the target follows a paragraph node # insert blank line, if the target follows a paragraph node
index = node.parent.index(node) index = node.parent.index(node)
if index > 0 and isinstance(node.parent[index - 1], nodes.paragraph): if index > 0 and isinstance(node.parent[index - 1], nodes.paragraph):
self.body.append('\n') self.body.append(CR)
# do not generate \phantomsection in \section{} # do not generate \phantomsection in \section{}
anchor = not self.in_title anchor = not self.in_title
@@ -1472,11 +1473,11 @@ class LaTeXTranslator(SphinxTranslator):
pass pass
def visit_attribution(self, node: Element) -> None: def visit_attribution(self, node: Element) -> None:
self.body.append('\n\\begin{flushright}\n') self.body.append(CR + '\\begin{flushright}' + CR)
self.body.append('---') self.body.append('---')
def depart_attribution(self, node: Element) -> None: def depart_attribution(self, node: Element) -> None:
self.body.append('\n\\end{flushright}\n') self.body.append(CR + '\\end{flushright}' + CR)
def visit_index(self, node: Element) -> None: def visit_index(self, node: Element) -> None:
def escape(value: str) -> str: def escape(value: str) -> str:
@@ -1497,7 +1498,7 @@ class LaTeXTranslator(SphinxTranslator):
return '\\spxentry{%s}' % string return '\\spxentry{%s}' % string
if not node.get('inline', True): if not node.get('inline', True):
self.body.append('\n') self.body.append(CR)
entries = node['entries'] entries = node['entries']
for type, string, tid, ismain, key_ in entries: for type, string, tid, ismain, key_ in entries:
m = '' m = ''
@@ -1546,11 +1547,11 @@ class LaTeXTranslator(SphinxTranslator):
def visit_raw(self, node: Element) -> None: def visit_raw(self, node: Element) -> None:
if not self.is_inline(node): if not self.is_inline(node):
self.body.append('\n') self.body.append(CR)
if 'latex' in node.get('format', '').split(): if 'latex' in node.get('format', '').split():
self.body.append(node.astext()) self.body.append(node.astext())
if not self.is_inline(node): if not self.is_inline(node):
self.body.append('\n') self.body.append(CR)
raise nodes.SkipNode raise nodes.SkipNode
def visit_reference(self, node: Element) -> None: def visit_reference(self, node: Element) -> None:
@@ -1559,7 +1560,7 @@ class LaTeXTranslator(SphinxTranslator):
anchor = not self.in_caption anchor = not self.in_caption
self.body += self.hypertarget(id, anchor=anchor) self.body += self.hypertarget(id, anchor=anchor)
if not self.is_inline(node): if not self.is_inline(node):
self.body.append('\n') self.body.append(CR)
uri = node.get('refuri', '') uri = node.get('refuri', '')
if not uri and node.get('refid'): if not uri and node.get('refid'):
uri = '%' + self.curfilestack[-1] + '#' + node['refid'] uri = '%' + self.curfilestack[-1] + '#' + node['refid']
@@ -1612,7 +1613,7 @@ class LaTeXTranslator(SphinxTranslator):
def depart_reference(self, node: Element) -> None: def depart_reference(self, node: Element) -> None:
self.body.append(self.context.pop()) self.body.append(self.context.pop())
if not self.is_inline(node): if not self.is_inline(node):
self.body.append('\n') self.body.append(CR)
def visit_number_reference(self, node: Element) -> None: def visit_number_reference(self, node: Element) -> None:
if node.get('refid'): if node.get('refid'):
@@ -1703,11 +1704,11 @@ class LaTeXTranslator(SphinxTranslator):
# adjust max width of citation labels not to break the layout # adjust max width of citation labels not to break the layout
longest_label = longest_label[:MAX_CITATION_LABEL_LENGTH] longest_label = longest_label[:MAX_CITATION_LABEL_LENGTH]
self.body.append('\n\\begin{sphinxthebibliography}{%s}\n' % self.body.append(CR + '\\begin{sphinxthebibliography}{%s}' %
self.encode(longest_label)) self.encode(longest_label) + CR)
def depart_thebibliography(self, node: Element) -> None: def depart_thebibliography(self, node: Element) -> None:
self.body.append('\\end{sphinxthebibliography}\n') self.body.append('\\end{sphinxthebibliography}' + CR)
def visit_citation(self, node: Element) -> None: def visit_citation(self, node: Element) -> None:
label = cast(nodes.label, node[0]) label = cast(nodes.label, node[0])
@@ -1749,15 +1750,15 @@ class LaTeXTranslator(SphinxTranslator):
def visit_footnotetext(self, node: Element) -> None: def visit_footnotetext(self, node: Element) -> None:
label = cast(nodes.label, node[0]) label = cast(nodes.label, node[0])
self.body.append('%\n') self.body.append('%' + CR)
self.body.append('\\begin{footnotetext}[%s]' self.body.append('\\begin{footnotetext}[%s]'
'\\phantomsection\\label{\\thesphinxscope.%s}%%\n' '\\phantomsection\\label{\\thesphinxscope.%s}%%'
% (label.astext(), label.astext())) % (label.astext(), label.astext()) + CR)
self.body.append('\\sphinxAtStartFootnote\n') self.body.append('\\sphinxAtStartFootnote' + CR)
def depart_footnotetext(self, node: Element) -> None: def depart_footnotetext(self, node: Element) -> None:
# the \ignorespaces in particular for after table header use # the \ignorespaces in particular for after table header use
self.body.append('%\n') self.body.append('%' + CR)
self.body.append('\\end{footnotetext}\\ignorespaces ') self.body.append('\\end{footnotetext}\\ignorespaces ')
def visit_captioned_literal_block(self, node: Element) -> None: def visit_captioned_literal_block(self, node: Element) -> None:
@@ -1770,13 +1771,13 @@ class LaTeXTranslator(SphinxTranslator):
if node.rawsource != node.astext(): if node.rawsource != node.astext():
# most probably a parsed-literal block -- don't highlight # most probably a parsed-literal block -- don't highlight
self.in_parsed_literal += 1 self.in_parsed_literal += 1
self.body.append('\\begin{sphinxalltt}\n') self.body.append('\\begin{sphinxalltt}' + CR)
else: else:
labels = self.hypertarget_to(node) labels = self.hypertarget_to(node)
if isinstance(node.parent, captioned_literal_block): if isinstance(node.parent, captioned_literal_block):
labels += self.hypertarget_to(node.parent) labels += self.hypertarget_to(node.parent)
if labels and not self.in_footnote: if labels and not self.in_footnote:
self.body.append('\n\\def\\sphinxLiteralBlockLabel{' + labels + '}') self.body.append(CR + '\\def\\sphinxLiteralBlockLabel{' + labels + '}')
lang = node.get('language', 'default') lang = node.get('language', 'default')
linenos = node.get('linenos', False) linenos = node.get('linenos', False)
@@ -1789,7 +1790,7 @@ class LaTeXTranslator(SphinxTranslator):
location=node, **highlight_args location=node, **highlight_args
) )
if self.in_footnote: if self.in_footnote:
self.body.append('\n\\sphinxSetupCodeBlockInFootnote') self.body.append(CR + '\\sphinxSetupCodeBlockInFootnote')
hlcode = hlcode.replace('\\begin{Verbatim}', hlcode = hlcode.replace('\\begin{Verbatim}',
'\\begin{sphinxVerbatim}') '\\begin{sphinxVerbatim}')
# if in table raise verbatim flag to avoid "tabulary" environment # if in table raise verbatim flag to avoid "tabulary" environment
@@ -1811,14 +1812,14 @@ class LaTeXTranslator(SphinxTranslator):
hllines = str(highlight_args.get('hl_lines', []))[1:-1] hllines = str(highlight_args.get('hl_lines', []))[1:-1]
if hllines: if hllines:
self.body.append('\n\\fvset{hllines={, %s,}}%%' % hllines) self.body.append(CR + '\\fvset{hllines={, %s,}}%%' % hllines)
self.body.append('\n' + hlcode + '\n') self.body.append(CR + hlcode + CR)
if hllines: if hllines:
self.body.append('\\sphinxresetverbatimhllines\n') self.body.append('\\sphinxresetverbatimhllines' + CR)
raise nodes.SkipNode raise nodes.SkipNode
def depart_literal_block(self, node: Element) -> None: def depart_literal_block(self, node: Element) -> None:
self.body.append('\n\\end{sphinxalltt}\n') self.body.append(CR + '\\end{sphinxalltt}' + CR)
self.in_parsed_literal -= 1 self.in_parsed_literal -= 1
visit_doctest_block = visit_literal_block visit_doctest_block = visit_literal_block
depart_doctest_block = depart_literal_block depart_doctest_block = depart_literal_block
@@ -1827,19 +1828,19 @@ class LaTeXTranslator(SphinxTranslator):
self.body.append('\\item[] ') self.body.append('\\item[] ')
def depart_line(self, node: Element) -> None: def depart_line(self, node: Element) -> None:
self.body.append('\n') self.body.append(CR)
def visit_line_block(self, node: Element) -> None: def visit_line_block(self, node: Element) -> None:
if isinstance(node.parent, nodes.line_block): if isinstance(node.parent, nodes.line_block):
self.body.append('\\item[]\n') self.body.append('\\item[]' + CR)
self.body.append('\\begin{DUlineblock}{\\DUlineblockindent}\n') self.body.append('\\begin{DUlineblock}{\\DUlineblockindent}' + CR)
else: else:
self.body.append('\n\\begin{DUlineblock}{0em}\n') self.body.append(CR + '\\begin{DUlineblock}{0em}' + CR)
if self.table: if self.table:
self.table.has_problematic = True self.table.has_problematic = True
def depart_line_block(self, node: Element) -> None: def depart_line_block(self, node: Element) -> None:
self.body.append('\\end{DUlineblock}\n') self.body.append('\\end{DUlineblock}' + CR)
def visit_block_quote(self, node: Element) -> None: def visit_block_quote(self, node: Element) -> None:
# If the block quote contains a single object and that object # If the block quote contains a single object and that object
@@ -1852,7 +1853,7 @@ class LaTeXTranslator(SphinxTranslator):
isinstance(child, nodes.enumerated_list): isinstance(child, nodes.enumerated_list):
done = 1 done = 1
if not done: if not done:
self.body.append('\\begin{quote}\n') self.body.append('\\begin{quote}' + CR)
if self.table: if self.table:
self.table.has_problematic = True self.table.has_problematic = True
@@ -1864,7 +1865,7 @@ class LaTeXTranslator(SphinxTranslator):
isinstance(child, nodes.enumerated_list): isinstance(child, nodes.enumerated_list):
done = 1 done = 1
if not done: if not done:
self.body.append('\\end{quote}\n') self.body.append('\\end{quote}' + CR)
# option node handling copied from docutils' latex writer # option node handling copied from docutils' latex writer
@@ -1894,12 +1895,12 @@ class LaTeXTranslator(SphinxTranslator):
self.body.append('] ') self.body.append('] ')
def visit_option_list(self, node: Element) -> None: def visit_option_list(self, node: Element) -> None:
self.body.append('\\begin{optionlist}{3cm}\n') self.body.append('\\begin{optionlist}{3cm}' + CR)
if self.table: if self.table:
self.table.has_problematic = True self.table.has_problematic = True
def depart_option_list(self, node: Element) -> None: def depart_option_list(self, node: Element) -> None:
self.body.append('\\end{optionlist}\n') self.body.append('\\end{optionlist}' + CR)
def visit_option_list_item(self, node: Element) -> None: def visit_option_list_item(self, node: Element) -> None:
pass pass
@@ -1992,7 +1993,7 @@ class LaTeXTranslator(SphinxTranslator):
if self.literal_whitespace: if self.literal_whitespace:
# Insert a blank before the newline, to avoid # Insert a blank before the newline, to avoid
# ! LaTeX Error: There's no line here to end. # ! LaTeX Error: There's no line here to end.
text = text.replace('\n', '~\\\\\n').replace(' ', '~') text = text.replace(CR, '~\\\\' + CR).replace(' ', '~')
return text return text
def encode_uri(self, text: str) -> str: def encode_uri(self, text: str) -> str:
@@ -2022,7 +2023,7 @@ class LaTeXTranslator(SphinxTranslator):
pass pass
def depart_system_message(self, node: Element) -> None: def depart_system_message(self, node: Element) -> None:
self.body.append('\n') self.body.append(CR)
def visit_math(self, node: Element) -> None: def visit_math(self, node: Element) -> None:
if self.in_title: if self.in_title:

View File

@@ -15,14 +15,14 @@ import textwrap
import time import time
import wsgiref.handlers import wsgiref.handlers
from datetime import datetime from datetime import datetime
from queue import Queue
from typing import Dict from typing import Dict
from unittest import mock from unittest import mock
import pytest import pytest
import requests import requests
from sphinx.builders.linkcheck import (CheckExternalLinksBuilder, from sphinx.builders.linkcheck import HyperlinkAvailabilityCheckWorker, RateLimit
HyperlinkAvailabilityCheckWorker, RateLimit)
from sphinx.util.console import strip_colors from sphinx.util.console import strip_colors
from .utils import CERT_FILE, http_server, https_server from .utils import CERT_FILE, http_server, https_server
@@ -536,10 +536,7 @@ class FakeResponse:
def test_limit_rate_default_sleep(app): def test_limit_rate_default_sleep(app):
checker = CheckExternalLinksBuilder(app) worker = HyperlinkAvailabilityCheckWorker(app.env, app.config, Queue(), Queue(), {})
checker.init()
checker.rate_limits = {}
worker = HyperlinkAvailabilityCheckWorker(checker)
with mock.patch('time.time', return_value=0.0): with mock.patch('time.time', return_value=0.0):
next_check = worker.limit_rate(FakeResponse()) next_check = worker.limit_rate(FakeResponse())
assert next_check == 60.0 assert next_check == 60.0
@@ -547,40 +544,34 @@ def test_limit_rate_default_sleep(app):
def test_limit_rate_user_max_delay(app): def test_limit_rate_user_max_delay(app):
app.config.linkcheck_rate_limit_timeout = 0.0 app.config.linkcheck_rate_limit_timeout = 0.0
checker = CheckExternalLinksBuilder(app) worker = HyperlinkAvailabilityCheckWorker(app.env, app.config, Queue(), Queue(), {})
checker.init()
checker.rate_limits = {}
worker = HyperlinkAvailabilityCheckWorker(checker)
next_check = worker.limit_rate(FakeResponse()) next_check = worker.limit_rate(FakeResponse())
assert next_check is None assert next_check is None
def test_limit_rate_doubles_previous_wait_time(app): def test_limit_rate_doubles_previous_wait_time(app):
checker = CheckExternalLinksBuilder(app) rate_limits = {"localhost": RateLimit(60.0, 0.0)}
checker.init() worker = HyperlinkAvailabilityCheckWorker(app.env, app.config, Queue(), Queue(),
checker.rate_limits = {"localhost": RateLimit(60.0, 0.0)} rate_limits)
worker = HyperlinkAvailabilityCheckWorker(checker)
with mock.patch('time.time', return_value=0.0): with mock.patch('time.time', return_value=0.0):
next_check = worker.limit_rate(FakeResponse()) next_check = worker.limit_rate(FakeResponse())
assert next_check == 120.0 assert next_check == 120.0
def test_limit_rate_clips_wait_time_to_max_time(app): def test_limit_rate_clips_wait_time_to_max_time(app):
checker = CheckExternalLinksBuilder(app)
checker.init()
app.config.linkcheck_rate_limit_timeout = 90.0 app.config.linkcheck_rate_limit_timeout = 90.0
checker.rate_limits = {"localhost": RateLimit(60.0, 0.0)} rate_limits = {"localhost": RateLimit(60.0, 0.0)}
worker = HyperlinkAvailabilityCheckWorker(checker) worker = HyperlinkAvailabilityCheckWorker(app.env, app.config, Queue(), Queue(),
rate_limits)
with mock.patch('time.time', return_value=0.0): with mock.patch('time.time', return_value=0.0):
next_check = worker.limit_rate(FakeResponse()) next_check = worker.limit_rate(FakeResponse())
assert next_check == 90.0 assert next_check == 90.0
def test_limit_rate_bails_out_after_waiting_max_time(app): def test_limit_rate_bails_out_after_waiting_max_time(app):
checker = CheckExternalLinksBuilder(app)
checker.init()
app.config.linkcheck_rate_limit_timeout = 90.0 app.config.linkcheck_rate_limit_timeout = 90.0
checker.rate_limits = {"localhost": RateLimit(90.0, 0.0)} rate_limits = {"localhost": RateLimit(90.0, 0.0)}
worker = HyperlinkAvailabilityCheckWorker(checker) worker = HyperlinkAvailabilityCheckWorker(app.env, app.config, Queue(), Queue(),
rate_limits)
next_check = worker.limit_rate(FakeResponse()) next_check = worker.limit_rate(FakeResponse())
assert next_check is None assert next_check is None