Skip to content

Commit c9d0933

Browse files
linkcheck: Use context managers for HTTP requests (#11318)
This closes HTTP responses when no content reads are required, as when requests are made in streaming mode, ``requests`` doesn't know whether the caller may intend to later read content from a streamed HTTP response object and holds the socket open. Co-authored-by: Adam Turner <9087854+AA-Turner@users.noreply.github.com>
1 parent 2b1c106 commit c9d0933

File tree

13 files changed

+38
-24
lines changed

13 files changed

+38
-24
lines changed

.github/workflows/main.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ env:
1313
FORCE_COLOR: "1"
1414
PYTHONDEVMODE: "1" # -X dev
1515
PYTHONWARNDEFAULTENCODING: "1" # -X warn_default_encoding
16-
PYTHONWARNINGS: "error,always:unclosed:ResourceWarning::" # default: all warnings as errors, except ResourceWarnings about unclosed items
16+
PYTHONWARNINGS: "error" # default: all warnings as errors
1717

1818
jobs:
1919
ubuntu:

sphinx/builders/linkcheck.py

+10-12
Original file line numberDiff line numberDiff line change
@@ -316,32 +316,30 @@ def check_uri() -> tuple[str, str, int]:
316316
try:
317317
if anchor and self.config.linkcheck_anchors:
318318
# Read the whole document and see if #anchor exists
319-
response = requests.get(req_url, stream=True, config=self.config,
320-
auth=auth_info, **kwargs)
321-
response.raise_for_status()
322-
found = check_anchor(response, unquote(anchor))
319+
with requests.get(req_url, stream=True, config=self.config, auth=auth_info,
320+
**kwargs) as response:
321+
response.raise_for_status()
322+
found = check_anchor(response, unquote(anchor))
323323

324324
if not found:
325325
raise Exception(__("Anchor '%s' not found") % anchor)
326326
else:
327327
try:
328328
# try a HEAD request first, which should be easier on
329329
# the server and the network
330-
response = requests.head(req_url, allow_redirects=True,
331-
config=self.config, auth=auth_info,
332-
**kwargs)
333-
response.raise_for_status()
330+
with requests.head(req_url, allow_redirects=True, config=self.config,
331+
auth=auth_info, **kwargs) as response:
332+
response.raise_for_status()
334333
# Servers drop the connection on HEAD requests, causing
335334
# ConnectionError.
336335
except (ConnectionError, HTTPError, TooManyRedirects) as err:
337336
if isinstance(err, HTTPError) and err.response.status_code == 429:
338337
raise
339338
# retry with GET request if that fails, some servers
340339
# don't like HEAD requests.
341-
response = requests.get(req_url, stream=True,
342-
config=self.config,
343-
auth=auth_info, **kwargs)
344-
response.raise_for_status()
340+
with requests.get(req_url, stream=True, config=self.config,
341+
auth=auth_info, **kwargs) as response:
342+
response.raise_for_status()
345343
except HTTPError as err:
346344
if err.response.status_code == 401:
347345
# We'll take "Unauthorized" as working.
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
exclude_patterns = ['_build']
22
linkcheck_anchors = True
3-
linkcheck_timeout = 0.075
3+
linkcheck_timeout = 0.05

tests/roots/test-linkcheck-documents_exclude/conf.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33
'^broken_link$',
44
'br[0-9]ken_link',
55
]
6-
linkcheck_timeout = 0.075
6+
linkcheck_timeout = 0.05
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
exclude_patterns = ['_build']
22
linkcheck_anchors = True
3-
linkcheck_timeout = 0.075
3+
linkcheck_timeout = 0.05
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
exclude_patterns = ['_build']
2-
linkcheck_timeout = 0.075
2+
linkcheck_timeout = 0.05
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
exclude_patterns = ['_build']
2-
linkcheck_timeout = 0.075
2+
linkcheck_timeout = 0.05
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
exclude_patterns = ['_build']
2-
linkcheck_timeout = 0.075
2+
linkcheck_timeout = 0.05
+1-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
exclude_patterns = ['_build']
2-
linkcheck_timeout = 0.075
2+
linkcheck_timeout = 0.05
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
exclude_patterns = ['_build']
22
linkcheck_anchors = True
3-
linkcheck_timeout = 0.075
3+
linkcheck_timeout = 0.05

tests/roots/test-linkcheck/conf.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
root_doc = 'links'
22
exclude_patterns = ['_build']
33
linkcheck_anchors = True
4-
linkcheck_timeout = 0.075
4+
linkcheck_timeout = 0.05

tests/roots/test-linkcheck/links.rst

+1
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ Some additional anchors to exercise ignore code
1111
.. image:: http://localhost:7777/image.png
1212
.. figure:: http://localhost:7777/image2.png
1313

14+
* `Valid anchored url <http://localhost:7777/anchor.html#found>`_

tests/test_build_linkcheck.py

+17-2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ def do_HEAD(self):
3131
if self.path[1:].rstrip() == "":
3232
self.send_response(200, "OK")
3333
self.end_headers()
34+
elif self.path[1:].rstrip() == "anchor.html":
35+
self.send_response(200, "OK")
36+
self.end_headers()
3437
else:
3538
self.send_response(404, "Not Found")
3639
self.end_headers()
@@ -39,6 +42,9 @@ def do_GET(self):
3942
self.do_HEAD()
4043
if self.path[1:].rstrip() == "":
4144
self.wfile.write(b"ok\n\n")
45+
elif self.path[1:].rstrip() == "anchor.html":
46+
doc = '<!DOCTYPE html><html><body><a id="found"></a></body></html>'
47+
self.wfile.write(doc.encode('utf-8'))
4248

4349

4450
@pytest.mark.sphinx('linkcheck', testroot='linkcheck', freshenv=True)
@@ -69,8 +75,8 @@ def test_defaults(app):
6975
for attr in ("filename", "lineno", "status", "code", "uri", "info"):
7076
assert attr in row
7177

72-
assert len(content.splitlines()) == 9
73-
assert len(rows) == 9
78+
assert len(content.splitlines()) == 10
79+
assert len(rows) == 10
7480
# the output order of the rows is not stable
7581
# due to possible variance in network latency
7682
rowsby = {row["uri"]: row for row in rows}
@@ -95,6 +101,15 @@ def test_defaults(app):
95101
assert rowsby["http://localhost:7777#does-not-exist"]["info"] == "Anchor 'does-not-exist' not found"
96102
# images should fail
97103
assert "Not Found for url: http://localhost:7777/image.png" in rowsby["http://localhost:7777/image.png"]["info"]
104+
# anchor should be found
105+
assert rowsby['http://localhost:7777/anchor.html#found'] == {
106+
'filename': 'links.rst',
107+
'lineno': 14,
108+
'status': 'working',
109+
'code': 0,
110+
'uri': 'http://localhost:7777/anchor.html#found',
111+
'info': '',
112+
}
98113

99114

100115
@pytest.mark.sphinx('linkcheck', testroot='linkcheck-too-many-retries', freshenv=True)

0 commit comments

Comments
 (0)