From 8f93ba24d53bbbc018ce188fb3bcd8759b9c48e4 Mon Sep 17 00:00:00 2001 From: Jeremy Maitin-Shepard Date: Mon, 30 Nov 2020 12:00:44 -0800 Subject: [PATCH 01/11] Support testing from read-only filesystems --- sphinx/testing/path.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/sphinx/testing/path.py b/sphinx/testing/path.py index a37839d60..1872f2577 100644 --- a/sphinx/testing/path.py +++ b/sphinx/testing/path.py @@ -98,6 +98,15 @@ class path(str): pointed to by the symbolic links are copied. """ shutil.copytree(self, destination, symlinks=symlinks) + # If source tree is marked read-only (e.g. because it is on a read-only + # filesystem), `shutil.copytree` will mark the destination as read-only + # as well. To avoid failures when adding additional files/directories + # to the destination tree, ensure destination directories are not marked + # read-only. + for root, dirs, files in os.walk(destination): + os.chmod(root, 0o755) + for name in files: + os.chmod(os.path.join(root, name), 0o644) def movetree(self, destination: str) -> None: """ From 654458b525d2a6c43f0bb5488297fdc20f34f93d Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Tue, 2 Feb 2021 23:26:14 +0900 Subject: [PATCH 02/11] Close #8813: Show what extension caused it on errors on event handler Show the module name of the event handler on the error inside it to let users know a hint of the error. --- CHANGES | 1 + sphinx/errors.py | 11 +++++++++-- sphinx/events.py | 4 +++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/CHANGES b/CHANGES index a4ab8bd51..9e3e29fe0 100644 --- a/CHANGES +++ b/CHANGES @@ -67,6 +67,7 @@ Features added dedent via no-argument ``:dedent:`` option * C++, also hyperlink operator overloads in expressions and alias declarations. * #8247: Allow production lists to refer to tokens from other production groups +* #8813: Show what extension (or module) caused it on errors on event handler Bugs fixed ---------- diff --git a/sphinx/errors.py b/sphinx/errors.py index c632d8dbc..3e84b6b88 100644 --- a/sphinx/errors.py +++ b/sphinx/errors.py @@ -47,12 +47,19 @@ class ApplicationError(SphinxError): class ExtensionError(SphinxError): """Extension error.""" - category = 'Extension error' - def __init__(self, message: str, orig_exc: Exception = None) -> None: + def __init__(self, message: str, orig_exc: Exception = None, modname: str = None) -> None: super().__init__(message) self.message = message self.orig_exc = orig_exc + self.modname = modname + + @property + def category(self) -> str: # type: ignore + if self.modname: + return 'Extension error (%s)' % self.modname + else: + return 'Extension error' def __repr__(self) -> str: if self.orig_exc: diff --git a/sphinx/events.py b/sphinx/events.py index 806a6e55d..e3a3b964f 100644 --- a/sphinx/events.py +++ b/sphinx/events.py @@ -19,6 +19,7 @@ from sphinx.deprecation import RemovedInSphinx40Warning from sphinx.errors import ExtensionError, SphinxError from sphinx.locale import __ from sphinx.util import logging +from sphinx.util.inspect import safe_getattr if False: # For type annotation @@ -114,8 +115,9 @@ class EventManager: except SphinxError: raise except Exception as exc: + modname = safe_getattr(listener.handler, '__module__', None) raise ExtensionError(__("Handler %r for event %r threw an exception") % - (listener.handler, name), exc) from exc + (listener.handler, name), exc, modname=modname) from exc return results def emit_firstresult(self, name: str, *args: Any, From 32257ee524ff33bb2ad623ed595346b986113a87 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Fri, 5 Feb 2021 00:17:44 +0900 Subject: [PATCH 03/11] test: Apply umask to Path.copytree() --- sphinx/testing/path.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/sphinx/testing/path.py b/sphinx/testing/path.py index 1872f2577..c6e47dc3c 100644 --- a/sphinx/testing/path.py +++ b/sphinx/testing/path.py @@ -18,6 +18,17 @@ from sphinx.deprecation import RemovedInSphinx50Warning FILESYSTEMENCODING = sys.getfilesystemencoding() or sys.getdefaultencoding() +def getumask() -> int: + """Get current umask value""" + umask = os.umask(0) # Note: Change umask value temporarily to obtain it + os.umask(umask) + + return umask + + +UMASK = getumask() + + class path(str): """ Represents a path which behaves like a string. @@ -104,9 +115,9 @@ class path(str): # to the destination tree, ensure destination directories are not marked # read-only. for root, dirs, files in os.walk(destination): - os.chmod(root, 0o755) + os.chmod(root, 0o755 & ~UMASK) for name in files: - os.chmod(os.path.join(root, name), 0o644) + os.chmod(os.path.join(root, name), 0o644 & ~UMASK) def movetree(self, destination: str) -> None: """ From 1fc7bc6b1e0790532f13a9c17bea9e0aec00b79c Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Fri, 5 Feb 2021 00:39:29 +0900 Subject: [PATCH 04/11] test: change permissions of testfiles only $SPHINX_READONLY_TESTDIR set --- sphinx/testing/path.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/sphinx/testing/path.py b/sphinx/testing/path.py index c6e47dc3c..7707a4525 100644 --- a/sphinx/testing/path.py +++ b/sphinx/testing/path.py @@ -109,15 +109,16 @@ class path(str): pointed to by the symbolic links are copied. """ shutil.copytree(self, destination, symlinks=symlinks) - # If source tree is marked read-only (e.g. because it is on a read-only - # filesystem), `shutil.copytree` will mark the destination as read-only - # as well. To avoid failures when adding additional files/directories - # to the destination tree, ensure destination directories are not marked - # read-only. - for root, dirs, files in os.walk(destination): - os.chmod(root, 0o755 & ~UMASK) - for name in files: - os.chmod(os.path.join(root, name), 0o644 & ~UMASK) + if os.environ.get('SPHINX_READONLY_TESTDIR'): + # If source tree is marked read-only (e.g. because it is on a read-only + # filesystem), `shutil.copytree` will mark the destination as read-only + # as well. To avoid failures when adding additional files/directories + # to the destination tree, ensure destination directories are not marked + # read-only. + for root, dirs, files in os.walk(destination): + os.chmod(root, 0o755 & ~UMASK) + for name in files: + os.chmod(os.path.join(root, name), 0o644 & ~UMASK) def movetree(self, destination: str) -> None: """ From b12a0f33ef8d31baa28860662a920708401c80d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Thu, 4 Feb 2021 21:53:49 +0100 Subject: [PATCH 05/11] Formalize linkcheck CheckResult into a NamedTuple --- sphinx/builders/linkcheck.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/sphinx/builders/linkcheck.py b/sphinx/builders/linkcheck.py index 70cd302d5..8877e2ed5 100644 --- a/sphinx/builders/linkcheck.py +++ b/sphinx/builders/linkcheck.py @@ -44,6 +44,12 @@ Hyperlink = NamedTuple('Hyperlink', (('next_check', float), ('uri', Optional[str]), ('docname', Optional[str]), ('lineno', Optional[int]))) +CheckResult = NamedTuple('CheckResult', (('uri', str), + ('docname', str), + ('lineno', int), + ('status', str), + ('message', str), + ('code', int))) RateLimit = NamedTuple('RateLimit', (('delay', float), ('next_check', float))) DEFAULT_REQUEST_HEADERS = { @@ -372,7 +378,7 @@ class CheckExternalLinksBuilder(DummyBuilder): self.rate_limits[netloc] = RateLimit(delay, next_check) return next_check - def process_result(self, result: Tuple[str, str, int, str, str, int]) -> None: + def process_result(self, result: CheckResult) -> None: uri, docname, lineno, status, info, code = result filename = self.env.doc2path(docname, None) @@ -443,8 +449,9 @@ class CheckExternalLinksBuilder(DummyBuilder): total_links = 0 for hyperlink in self.hyperlinks.values(): if self.is_ignored_uri(hyperlink.uri): - self.process_result((hyperlink.uri, hyperlink.docname, hyperlink.lineno, - 'ignored', '', 0)) + self.process_result( + CheckResult(hyperlink.uri, hyperlink.docname, hyperlink.lineno, + 'ignored', '', 0)) else: self.wqueue.put(hyperlink, False) total_links += 1 From 00e2c2e2502dd27c6726586cde921d0981e74c02 Mon Sep 17 00:00:00 2001 From: Thibaud Colas Date: Thu, 4 Feb 2021 22:04:17 +0000 Subject: [PATCH 06/11] Fix broken "Extend" link in site header Spotted while navigating https://www.sphinx-doc.org/en/master/. --- doc/_themes/sphinx13/layout.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/_themes/sphinx13/layout.html b/doc/_themes/sphinx13/layout.html index 238fb52b7..b7f7c1424 100644 --- a/doc/_themes/sphinx13/layout.html +++ b/doc/_themes/sphinx13/layout.html @@ -67,7 +67,7 @@
  • Home
  • Get it
  • Docs
  • -
  • Extend
  • +
  • Extend
  • From f02fb7a8cc23a44a392597109d9e6276b5100583 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Sun, 31 Jan 2021 17:45:38 +0900 Subject: [PATCH 07/11] refactor: linkcheck: Separate worker feature from builder class To reduce the complexity of the linkcheck builder, this separates the worker feature from the builder class. --- sphinx/builders/linkcheck.py | 225 ++++++++++++++++++++-------------- tests/test_build_linkcheck.py | 23 +++- 2 files changed, 148 insertions(+), 100 deletions(-) diff --git a/sphinx/builders/linkcheck.py b/sphinx/builders/linkcheck.py index 8877e2ed5..0c452ae4a 100644 --- a/sphinx/builders/linkcheck.py +++ b/sphinx/builders/linkcheck.py @@ -12,13 +12,13 @@ import json import queue import re import socket -import threading import time import warnings from datetime import datetime, timezone from email.utils import parsedate_to_datetime from html.parser import HTMLParser from os import path +from threading import Thread from typing import Any, Dict, List, NamedTuple, Optional, Set, Tuple, cast from urllib.parse import unquote, urlparse @@ -129,9 +129,9 @@ class CheckExternalLinksBuilder(DummyBuilder): self.rate_limits = {} # type: Dict[str, RateLimit] self.wqueue = queue.PriorityQueue() # type: queue.PriorityQueue self.rqueue = queue.Queue() # type: queue.Queue - self.workers = [] # type: List[threading.Thread] + self.workers = [] # type: List[Thread] for i in range(self.config.linkcheck_workers): - thread = threading.Thread(target=self.check_thread, daemon=True) + thread = HyperlinkAvailabilityCheckWorker(self) thread.start() self.workers.append(thread) @@ -166,6 +166,134 @@ class CheckExternalLinksBuilder(DummyBuilder): return self._redirected def check_thread(self) -> None: + warnings.warn( + "%s.%s is deprecated." % (self.__class__.__name__, "check_thread"), + RemovedInSphinx50Warning, + stacklevel=2, + ) + # do nothing. + + def limit_rate(self, response: Response) -> Optional[float]: + warnings.warn( + "%s.%s is deprecated." % (self.__class__.__name__, "limit_rate"), + RemovedInSphinx50Warning, + stacklevel=2, + ) + return HyperlinkAvailabilityCheckWorker(self).limit_rate(response) + + def process_result(self, result: Tuple[str, str, int, str, str, int]) -> None: + uri, docname, lineno, status, info, code = result + + filename = self.env.doc2path(docname, None) + linkstat = dict(filename=filename, lineno=lineno, + status=status, code=code, uri=uri, + info=info) + if status == 'unchecked': + self.write_linkstat(linkstat) + return + if status == 'working' and info == 'old': + self.write_linkstat(linkstat) + return + if lineno: + logger.info('(%16s: line %4d) ', docname, lineno, nonl=True) + if status == 'ignored': + if info: + logger.info(darkgray('-ignored- ') + uri + ': ' + info) + else: + logger.info(darkgray('-ignored- ') + uri) + self.write_linkstat(linkstat) + elif status == 'local': + logger.info(darkgray('-local- ') + uri) + self.write_entry('local', docname, filename, lineno, uri) + self.write_linkstat(linkstat) + elif status == 'working': + logger.info(darkgreen('ok ') + uri + info) + self.write_linkstat(linkstat) + elif status == 'broken': + if self.app.quiet or self.app.warningiserror: + logger.warning(__('broken link: %s (%s)'), uri, info, + location=(filename, lineno)) + else: + logger.info(red('broken ') + uri + red(' - ' + info)) + self.write_entry('broken', docname, filename, lineno, uri + ': ' + info) + self.write_linkstat(linkstat) + elif status == 'redirected': + try: + text, color = { + 301: ('permanently', purple), + 302: ('with Found', purple), + 303: ('with See Other', purple), + 307: ('temporarily', turquoise), + 308: ('permanently', purple), + }[code] + except KeyError: + text, color = ('with unknown code', purple) + linkstat['text'] = text + logger.info(color('redirect ') + uri + color(' - ' + text + ' to ' + info)) + self.write_entry('redirected ' + text, docname, filename, + lineno, uri + ' to ' + info) + self.write_linkstat(linkstat) + else: + raise ValueError("Unknown status %s." % status) + + def write_entry(self, what: str, docname: str, filename: str, line: int, + uri: str) -> None: + self.txt_outfile.write("%s:%s: [%s] %s\n" % (filename, line, what, uri)) + + def write_linkstat(self, data: dict) -> None: + self.json_outfile.write(json.dumps(data)) + self.json_outfile.write('\n') + + def finish(self) -> None: + logger.info('') + + 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: + total_links = 0 + for hyperlink in self.hyperlinks.values(): + 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: + self.app.statuscode = 1 + + self.wqueue.join() + # Shutdown threads. + for worker in self.workers: + self.wqueue.put((CHECK_IMMEDIATELY, None, None, None), False) + + +class HyperlinkAvailabilityCheckWorker(Thread): + """A worker class for checing the availability of hyperlinks.""" + + def __init__(self, builder: CheckExternalLinksBuilder) -> None: + self.app = builder.app + self.anchors_ignore = builder.anchors_ignore + self.auth = builder.auth + self.config = builder.config + self.env = builder.env + self.rate_limits = builder.rate_limits + self.rqueue = builder.rqueue + self.to_ignore = builder.to_ignore + self.wqueue = builder.wqueue + + self._good = builder._good + self._broken = builder._broken + self._redirected = builder._redirected + + super().__init__(daemon=True) + + def run(self) -> None: kwargs = {} if self.config.linkcheck_timeout: kwargs['timeout'] = self.config.linkcheck_timeout @@ -378,97 +506,6 @@ class CheckExternalLinksBuilder(DummyBuilder): self.rate_limits[netloc] = RateLimit(delay, next_check) return next_check - def process_result(self, result: CheckResult) -> None: - uri, docname, lineno, status, info, code = result - - filename = self.env.doc2path(docname, None) - linkstat = dict(filename=filename, lineno=lineno, - status=status, code=code, uri=uri, - info=info) - if status == 'unchecked': - self.write_linkstat(linkstat) - return - if status == 'working' and info == 'old': - self.write_linkstat(linkstat) - return - if lineno: - logger.info('(%16s: line %4d) ', docname, lineno, nonl=True) - if status == 'ignored': - if info: - logger.info(darkgray('-ignored- ') + uri + ': ' + info) - else: - logger.info(darkgray('-ignored- ') + uri) - self.write_linkstat(linkstat) - elif status == 'local': - logger.info(darkgray('-local- ') + uri) - self.write_entry('local', docname, filename, lineno, uri) - self.write_linkstat(linkstat) - elif status == 'working': - logger.info(darkgreen('ok ') + uri + info) - self.write_linkstat(linkstat) - elif status == 'broken': - if self.app.quiet or self.app.warningiserror: - logger.warning(__('broken link: %s (%s)'), uri, info, - location=(filename, lineno)) - else: - logger.info(red('broken ') + uri + red(' - ' + info)) - self.write_entry('broken', docname, filename, lineno, uri + ': ' + info) - self.write_linkstat(linkstat) - elif status == 'redirected': - try: - text, color = { - 301: ('permanently', purple), - 302: ('with Found', purple), - 303: ('with See Other', purple), - 307: ('temporarily', turquoise), - 308: ('permanently', purple), - }[code] - except KeyError: - text, color = ('with unknown code', purple) - linkstat['text'] = text - logger.info(color('redirect ') + uri + color(' - ' + text + ' to ' + info)) - self.write_entry('redirected ' + text, docname, filename, - lineno, uri + ' to ' + info) - self.write_linkstat(linkstat) - else: - raise ValueError("Unknown status %s." % status) - - def write_entry(self, what: str, docname: str, filename: str, line: int, - uri: str) -> None: - self.txt_outfile.write("%s:%s: [%s] %s\n" % (filename, line, what, uri)) - - def write_linkstat(self, data: dict) -> None: - self.json_outfile.write(json.dumps(data)) - self.json_outfile.write('\n') - - def finish(self) -> None: - logger.info('') - - 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: - total_links = 0 - for hyperlink in self.hyperlinks.values(): - 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: - self.app.statuscode = 1 - - self.wqueue.join() - # Shutdown threads. - for worker in self.workers: - self.wqueue.put((CHECK_IMMEDIATELY, None, None, None), False) - class HyperlinkCollector(SphinxPostTransform): builders = ('linkcheck',) diff --git a/tests/test_build_linkcheck.py b/tests/test_build_linkcheck.py index 60b62435c..e297d42c4 100644 --- a/tests/test_build_linkcheck.py +++ b/tests/test_build_linkcheck.py @@ -21,7 +21,8 @@ from unittest import mock import pytest import requests -from sphinx.builders.linkcheck import CheckExternalLinksBuilder, RateLimit +from sphinx.builders.linkcheck import (CheckExternalLinksBuilder, + HyperlinkAvailabilityCheckWorker, RateLimit) from sphinx.util.console import strip_colors from .utils import CERT_FILE, http_server, https_server @@ -536,40 +537,50 @@ class FakeResponse: def test_limit_rate_default_sleep(app): checker = CheckExternalLinksBuilder(app) + checker.init() checker.rate_limits = {} + worker = HyperlinkAvailabilityCheckWorker(checker) with mock.patch('time.time', return_value=0.0): - next_check = checker.limit_rate(FakeResponse()) + next_check = worker.limit_rate(FakeResponse()) assert next_check == 60.0 def test_limit_rate_user_max_delay(app): app.config.linkcheck_rate_limit_timeout = 0.0 checker = CheckExternalLinksBuilder(app) + checker.init() checker.rate_limits = {} - next_check = checker.limit_rate(FakeResponse()) + worker = HyperlinkAvailabilityCheckWorker(checker) + next_check = worker.limit_rate(FakeResponse()) assert next_check is None def test_limit_rate_doubles_previous_wait_time(app): checker = CheckExternalLinksBuilder(app) + checker.init() checker.rate_limits = {"localhost": RateLimit(60.0, 0.0)} + worker = HyperlinkAvailabilityCheckWorker(checker) with mock.patch('time.time', return_value=0.0): - next_check = checker.limit_rate(FakeResponse()) + next_check = worker.limit_rate(FakeResponse()) assert next_check == 120.0 def test_limit_rate_clips_wait_time_to_max_time(app): checker = CheckExternalLinksBuilder(app) + checker.init() app.config.linkcheck_rate_limit_timeout = 90.0 checker.rate_limits = {"localhost": RateLimit(60.0, 0.0)} + worker = HyperlinkAvailabilityCheckWorker(checker) with mock.patch('time.time', return_value=0.0): - next_check = checker.limit_rate(FakeResponse()) + next_check = worker.limit_rate(FakeResponse()) assert next_check == 90.0 def test_limit_rate_bails_out_after_waiting_max_time(app): checker = CheckExternalLinksBuilder(app) + checker.init() app.config.linkcheck_rate_limit_timeout = 90.0 checker.rate_limits = {"localhost": RateLimit(90.0, 0.0)} - next_check = checker.limit_rate(FakeResponse()) + worker = HyperlinkAvailabilityCheckWorker(checker) + next_check = worker.limit_rate(FakeResponse()) assert next_check is None From 84130fff40102c29e4969444ae7b536c6ce4d7a3 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Sat, 6 Feb 2021 01:24:16 +0900 Subject: [PATCH 08/11] Fix typo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: François Freitag --- sphinx/builders/linkcheck.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinx/builders/linkcheck.py b/sphinx/builders/linkcheck.py index 0c452ae4a..590eec201 100644 --- a/sphinx/builders/linkcheck.py +++ b/sphinx/builders/linkcheck.py @@ -274,7 +274,7 @@ class CheckExternalLinksBuilder(DummyBuilder): class HyperlinkAvailabilityCheckWorker(Thread): - """A worker class for checing the availability of hyperlinks.""" + """A worker class for checking the availability of hyperlinks.""" def __init__(self, builder: CheckExternalLinksBuilder) -> None: self.app = builder.app From ad5b0babd7405f8740be9e1cb65a11feaf60915e Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Sat, 6 Feb 2021 01:31:22 +0900 Subject: [PATCH 09/11] refactor: linkcheck: Remove unused attribute HyperlinkAvailabilityCheckWorker.app --- sphinx/builders/linkcheck.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sphinx/builders/linkcheck.py b/sphinx/builders/linkcheck.py index 590eec201..659305d32 100644 --- a/sphinx/builders/linkcheck.py +++ b/sphinx/builders/linkcheck.py @@ -277,7 +277,6 @@ class HyperlinkAvailabilityCheckWorker(Thread): """A worker class for checking the availability of hyperlinks.""" def __init__(self, builder: CheckExternalLinksBuilder) -> None: - self.app = builder.app self.anchors_ignore = builder.anchors_ignore self.auth = builder.auth self.config = builder.config From 38b14bbfef5d5d498cdcd2a9a069934992a06007 Mon Sep 17 00:00:00 2001 From: jfbu Date: Sat, 6 Feb 2021 16:35:16 +0100 Subject: [PATCH 10/11] Revert "Add \nobreak inside \sphinxAtStartPar" This reverts commit 17642a5e6bbcccc5616da381c10a068c9e09b6d7. Fixes #8838. For some reason the \nobreak causes breakage in table vertical spacing for merge cells in tabular and longtable, and regarding tabulary for all cells... Reverting this will cause as described in the reverted commit message some much less annoying problem in certain circumstances when a long word has no hyphenation point. --- sphinx/texinputs/sphinx.sty | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinx/texinputs/sphinx.sty b/sphinx/texinputs/sphinx.sty index eb5617dda..da03ff989 100644 --- a/sphinx/texinputs/sphinx.sty +++ b/sphinx/texinputs/sphinx.sty @@ -414,7 +414,7 @@ \DisableKeyvalOption{sphinx}{mathnumfig} % To allow hyphenation of first word in narrow contexts; no option, % customization to be done via 'preamble' key -\newcommand*\sphinxAtStartPar{\nobreak\hskip\z@skip} +\newcommand*\sphinxAtStartPar{\hskip\z@skip} % No need for the \hspace{0pt} trick (\hskip\z@skip) with luatex \ifdefined\directlua\let\sphinxAtStartPar\@empty\fi % user interface: options can be changed midway in a document! From 899ccfd40ead50d35f6c7244ea7dad367ab71073 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Thu, 4 Feb 2021 01:56:16 +0900 Subject: [PATCH 11/11] refactor: linkcheck: Deprecate attributes of linkcheck builders Move anchors_ignore, auth and to_ignore to HyperlinkAvailabilityCheckWorker and become deprecated. --- CHANGES | 3 +++ doc/extdev/deprecated.rst | 15 ++++++++++++ sphinx/builders/linkcheck.py | 44 ++++++++++++++++++++++++++++-------- 3 files changed, 53 insertions(+), 9 deletions(-) diff --git a/CHANGES b/CHANGES index 9e3e29fe0..b394c54e3 100644 --- a/CHANGES +++ b/CHANGES @@ -18,9 +18,12 @@ Deprecated ---------- * pending_xref node for viewcode extension +* ``sphinx.builders.linkcheck.CheckExternalLinksBuilder.anchors_ignore`` +* ``sphinx.builders.linkcheck.CheckExternalLinksBuilder.auth`` * ``sphinx.builders.linkcheck.CheckExternalLinksBuilder.broken`` * ``sphinx.builders.linkcheck.CheckExternalLinksBuilder.good`` * ``sphinx.builders.linkcheck.CheckExternalLinksBuilder.redirected`` +* ``sphinx.builders.linkcheck.CheckExternalLinksBuilder.to_ignore`` * ``sphinx.builders.linkcheck.node_line_or_0()`` * ``sphinx.ext.autodoc.AttributeDocumenter.isinstanceattribute()`` * ``sphinx.ext.autodoc.directive.DocumenterBridge.reporter`` diff --git a/doc/extdev/deprecated.rst b/doc/extdev/deprecated.rst index 1350085ef..dbebb840c 100644 --- a/doc/extdev/deprecated.rst +++ b/doc/extdev/deprecated.rst @@ -27,6 +27,16 @@ The following is a list of deprecated interfaces. - 5.0 - ``sphinx.ext.viewcode.viewcode_anchor`` + * - ``sphinx.builders.linkcheck.CheckExternalLinksBuilder.anchors_ignore`` + - 3.5 + - 5.0 + - N/A + + * - ``sphinx.builders.linkcheck.CheckExternalLinksBuilder.auth`` + - 3.5 + - 5.0 + - N/A + * - ``sphinx.builders.linkcheck.CheckExternalLinksBuilder.broken`` - 3.5 - 5.0 @@ -42,6 +52,11 @@ The following is a list of deprecated interfaces. - 5.0 - N/A + * - ``sphinx.builders.linkcheck.CheckExternalLinksBuilder.to_ignore`` + - 3.5 + - 5.0 + - N/A + * - ``sphinx.builders.linkcheck.node_line_or_0()`` - 3.5 - 5.0 diff --git a/sphinx/builders/linkcheck.py b/sphinx/builders/linkcheck.py index 659305d32..900346def 100644 --- a/sphinx/builders/linkcheck.py +++ b/sphinx/builders/linkcheck.py @@ -19,7 +19,7 @@ from email.utils import parsedate_to_datetime from html.parser import HTMLParser from os import path from threading import Thread -from typing import Any, Dict, List, NamedTuple, Optional, Set, Tuple, cast +from typing import Any, Dict, List, NamedTuple, Optional, Pattern, Set, Tuple, cast from urllib.parse import unquote, urlparse from docutils import nodes @@ -114,11 +114,6 @@ class CheckExternalLinksBuilder(DummyBuilder): def init(self) -> None: self.hyperlinks = {} # type: Dict[str, Hyperlink] - self.to_ignore = [re.compile(x) for x in self.config.linkcheck_ignore] - self.anchors_ignore = [re.compile(x) - for x in self.config.linkcheck_anchors_ignore] - self.auth = [(re.compile(pattern), auth_info) for pattern, auth_info - in self.config.linkcheck_auth] self._good = set() # type: Set[str] self._broken = {} # type: Dict[str, str] self._redirected = {} # type: Dict[str, Tuple[str, int]] @@ -138,6 +133,34 @@ class CheckExternalLinksBuilder(DummyBuilder): def is_ignored_uri(self, uri: str) -> bool: return any(pat.match(uri) for pat in self.to_ignore) + @property + def anchors_ignore(self) -> List[Pattern]: + warnings.warn( + "%s.%s is deprecated." % (self.__class__.__name__, "anchors_ignore"), + RemovedInSphinx50Warning, + stacklevel=2, + ) + return [re.compile(x) for x in self.config.linkcheck_anchors_ignore] + + @property + def auth(self) -> List[Tuple[Pattern, Any]]: + warnings.warn( + "%s.%s is deprecated." % (self.__class__.__name__, "auth"), + RemovedInSphinx50Warning, + stacklevel=2, + ) + return [(re.compile(pattern), auth_info) for pattern, auth_info + in self.config.linkcheck_auth] + + @property + def to_ignore(self) -> List[Pattern]: + warnings.warn( + "%s.%s is deprecated." % (self.__class__.__name__, "to_ignore"), + RemovedInSphinx50Warning, + stacklevel=2, + ) + return [re.compile(x) for x in self.config.linkcheck_ignore] + @property def good(self) -> Set[str]: warnings.warn( @@ -277,15 +300,18 @@ class HyperlinkAvailabilityCheckWorker(Thread): """A worker class for checking the availability of hyperlinks.""" def __init__(self, builder: CheckExternalLinksBuilder) -> None: - self.anchors_ignore = builder.anchors_ignore - self.auth = builder.auth self.config = builder.config self.env = builder.env self.rate_limits = builder.rate_limits self.rqueue = builder.rqueue - self.to_ignore = builder.to_ignore self.wqueue = builder.wqueue + self.anchors_ignore = [re.compile(x) + for x in self.config.linkcheck_anchors_ignore] + self.auth = [(re.compile(pattern), auth_info) for pattern, auth_info + in self.config.linkcheck_auth] + self.to_ignore = [re.compile(x) for x in self.config.linkcheck_ignore] + self._good = builder._good self._broken = builder._broken self._redirected = builder._redirected