Skip to content

Commit 5ecb765

Browse files
committed
Use new WeasyPrint URL fetcher
Fix #35.
1 parent 58f9a9f commit 5ecb765

File tree

3 files changed

+69
-66
lines changed

3 files changed

+69
-66
lines changed

flask_weasyprint/__init__.py

Lines changed: 36 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22

33
from io import BytesIO
44
from urllib.parse import urljoin, urlsplit
5+
from urllib.request import BaseHandler
56

67
from flask import current_app, has_request_context, request, send_file
7-
from werkzeug.test import Client, ClientRedirectError, EnvironBuilder
8+
from werkzeug.test import Client, EnvironBuilder
89
from werkzeug.wrappers import Response
910

1011
VERSION = __version__ = '1.1.0'
@@ -73,7 +74,7 @@ def dispatch(url_string):
7374

7475

7576
def make_url_fetcher(dispatcher=None, next_fetcher=True):
76-
"""Return an function suitable as a ``url_fetcher`` in WeasyPrint.
77+
"""Return a URL fetcher that handles the Flask app routes internally.
7778
7879
You generally don’t need to call this directly.
7980
@@ -92,45 +93,44 @@ def make_url_fetcher(dispatcher=None, next_fetcher=True):
9293
Typically ``base_url + path`` is equivalent to the passed URL.
9394
9495
"""
96+
from weasyprint.urls import URLFetcher, URLFetcherResponse # lazy loading
97+
9598
if next_fetcher is True:
96-
from weasyprint import default_url_fetcher # lazy loading
97-
next_fetcher = default_url_fetcher
99+
next_fetcher = URLFetcher
98100

99101
if dispatcher is None:
100102
dispatcher = make_flask_url_dispatcher()
101103

102-
def flask_url_fetcher(url):
103-
redirect_chain = set()
104-
while True:
105-
result = dispatcher(url)
106-
if result is None:
107-
return next_fetcher(url)
108-
app, base_url, path = result
109-
client = Client(app, response_wrapper=Response)
110-
if has_request_context() and request.cookies:
111-
server_name = EnvironBuilder(
112-
path, base_url=base_url).server_name
113-
for cookie_key, cookie_value in request.cookies.items():
114-
client.set_cookie(
115-
cookie_key, cookie_value, domain=server_name)
116-
response = client.get(path, base_url=base_url)
117-
if response.status_code == 200:
118-
return {
119-
'string': response.data, 'mime_type': response.mimetype,
120-
'encoding': 'utf-8', 'redirected_url': url}
121-
# The test client can follow redirects, but do it ourselves
122-
# to get access to the redirected URL.
123-
elif response.status_code in (301, 302, 303, 305, 307, 308):
124-
redirect_chain.add(url)
125-
url = urljoin(url, response.location)
126-
if url in redirect_chain:
127-
raise ClientRedirectError('loop detected')
128-
else:
129-
raise ValueError(
130-
'Flask-WeasyPrint got HTTP status '
131-
f'{response.status} for {urljoin(base_url, path)}')
132-
133-
return flask_url_fetcher
104+
class FlaskHandler(BaseHandler):
105+
def default_open(self, req):
106+
url = req.full_url
107+
if result := dispatcher(url):
108+
app, base_url, path = result
109+
client = Client(app, response_wrapper=Response)
110+
if has_request_context() and request.cookies:
111+
server_name = EnvironBuilder(path, base_url=base_url).server_name
112+
for cookie_key, cookie_value in request.cookies.items():
113+
client.set_cookie(cookie_key, cookie_value, domain=server_name)
114+
response = client.get(path, base_url=base_url)
115+
response = URLFetcherResponse(
116+
url, response.data, response.headers, response.status_code)
117+
response.msg = ''
118+
return response
119+
120+
class FlaskFetcher(next_fetcher or URLFetcher):
121+
def __init__(self, *args, **kwargs):
122+
super().__init__(*args, **kwargs)
123+
self.add_handler(FlaskHandler())
124+
125+
def fetch(self, url, headers=None):
126+
if dispatcher(url) is None:
127+
if next_fetcher:
128+
return super().fetch(url, headers)
129+
else:
130+
raise ValueError(f'Unknown Flask app URL: {url}')
131+
return URLFetcher.fetch(self, url, headers)
132+
133+
return FlaskFetcher()
134134

135135

136136
def _wrapper(class_, *args, **kwargs):

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ readme = {file = 'README.rst', content-type = 'text/x-rst'}
1313
license = {file = 'LICENSE'}
1414
dependencies = [
1515
'flask >=2.3.0',
16-
'weasyprint >=53.0',
16+
'weasyprint >=68.0',
1717
]
1818
classifiers = [
1919
'Development Status :: 5 - Production/Stable',

tests/test_flask_weasyprint.py

Lines changed: 32 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
"""Tests for Flask-WeasyPrint."""
22

3+
from urllib.error import HTTPError
4+
35
import pytest
46
from flask import Flask, json, jsonify, redirect, request
57
from weasyprint import __version__ as weasyprint_version
6-
from werkzeug.test import ClientRedirectError
8+
from weasyprint.urls import URLFetcher, URLFetcherResponse
79

810
from flask_weasyprint import CSS, HTML, make_url_fetcher, render_pdf
911

@@ -19,25 +21,25 @@ def test_url_fetcher():
1921
with app.test_request_context(base_url='http://example.org/bar/'):
2022
fetcher = make_url_fetcher()
2123

22-
result = fetcher('http://example.org/bar/')
23-
assert result['string'].strip().startswith(b'<html>')
24-
assert result['mime_type'] == 'text/html'
25-
assert result['encoding'] == 'utf-8'
26-
assert result['redirected_url'] == 'http://example.org/bar/foo/'
24+
response = fetcher('http://example.org/bar/')
25+
assert response.read().strip().startswith(b'<html>')
26+
assert response.content_type == 'text/html'
27+
assert response.charset == 'utf-8'
28+
assert response.url == 'http://example.org/bar/foo/'
2729

28-
result = fetcher('http://example.org/bar/foo/graph?data=1&labels=A')
29-
assert result['string'].strip().startswith(b'<svg xmlns=')
30-
assert result['mime_type'] == 'image/svg+xml'
30+
response = fetcher('http://example.org/bar/foo/graph?data=1&labels=A')
31+
assert response.read().strip().startswith(b'<svg xmlns=')
32+
assert response.content_type == 'image/svg+xml'
3133

3234
# Also works with a custom dispatcher
3335
def custom_dispatcher(url_string):
3436
return app, 'http://example.org/bar/', '/foo/graph?data=1&labels=A'
3537
with app.test_request_context(base_url='http://example.org/bar/'):
3638
fetcher = make_url_fetcher(dispatcher=custom_dispatcher)
3739

38-
result = fetcher('test://')
39-
assert result['string'].strip().startswith(b'<svg xmlns=')
40-
assert result['mime_type'] == 'image/svg+xml'
40+
response = fetcher('test://')
41+
assert response.read().strip().startswith(b'<svg xmlns=')
42+
assert response.content_type == 'image/svg+xml'
4143

4244

4345
def test_wrappers():
@@ -103,12 +105,14 @@ def add_redirect(old_url, new_url):
103105

104106
with app.test_request_context():
105107
fetcher = make_url_fetcher()
106-
result = fetcher('http://localhost/a')
107-
assert result['string'] == b'Ok'
108-
assert result['redirected_url'] == 'http://localhost/d'
109-
with pytest.raises(ClientRedirectError):
108+
response = fetcher('http://localhost/a')
109+
assert response.read() == b'Ok'
110+
assert response.url == 'http://localhost/d'
111+
# TODO: keep HTTPError with WeasyPrint > 68.1, see #2686.
112+
# with pytest.raises(HTTPError, match='infinite loop'):
113+
with pytest.raises((HTTPError, RecursionError)):
110114
fetcher('http://localhost/1')
111-
with pytest.raises(ValueError, match='404'):
115+
with pytest.raises(HTTPError, match='404'):
112116
fetcher('http://localhost/nonexistent')
113117

114118

@@ -125,21 +129,22 @@ def catchall(subdomain='', path=None):
125129
app = [subdomain, request.script_root, request.path, query_string]
126130
return jsonify(app=app)
127131

128-
def dummy_fetcher(url):
129-
return {'string': 'dummy ' + url}
132+
class DummyFetcher(URLFetcher):
133+
def fetch(self, url, headers=None):
134+
return URLFetcherResponse(url, f'dummy {url}')
130135

131136
def assert_app(url, host, script_root, path, query_string=''):
132137
"""The URL was dispatched to the app with these parameters."""
133-
assert json.loads(dispatcher(url)['string']) == {
138+
assert json.loads(dispatcher(url).read()) == {
134139
'app': [host, script_root, path, query_string]}
135140

136141
def assert_dummy(url):
137142
"""The URL was not dispatched, the default fetcher was used."""
138-
assert dispatcher(url)['string'] == 'dummy ' + url
143+
assert dispatcher(url).read() == f'dummy {url}'.encode()
139144

140145
# No SERVER_NAME config, default port
141146
with app.test_request_context(base_url='http://a.net/b/'):
142-
dispatcher = make_url_fetcher(next_fetcher=dummy_fetcher)
147+
dispatcher = make_url_fetcher(next_fetcher=DummyFetcher)
143148
assert_app('http://a.net/b', '', '/b', '/')
144149
assert_app('http://a.net/b/', '', '/b', '/')
145150
assert_app('http://a.net/b/', '', '/b', '/')
@@ -153,7 +158,7 @@ def assert_dummy(url):
153158

154159
# No SERVER_NAME config, explicit default port
155160
with app.test_request_context(base_url='http://a.net:80/b/'):
156-
dispatcher = make_url_fetcher(next_fetcher=dummy_fetcher)
161+
dispatcher = make_url_fetcher(next_fetcher=DummyFetcher)
157162
assert_app('http://a.net/b', '', '/b', '/')
158163
assert_app('http://a.net/b/', '', '/b', '/')
159164
assert_app('http://a.net/b/c/d?e', '', '/b', '/c/d', 'e')
@@ -166,7 +171,7 @@ def assert_dummy(url):
166171

167172
# Change the context’s port number
168173
with app.test_request_context(base_url='http://a.net:8888/b/'):
169-
dispatcher = make_url_fetcher(next_fetcher=dummy_fetcher)
174+
dispatcher = make_url_fetcher(next_fetcher=DummyFetcher)
170175
assert_app('http://a.net:8888/b', '', '/b', '/')
171176
assert_app('http://a.net:8888/b/', '', '/b', '/')
172177
assert_app('http://a.net:8888/b/cd?e', '', '/b', '/cd', 'e')
@@ -181,7 +186,7 @@ def assert_dummy(url):
181186
# Add a SERVER_NAME config
182187
app.config['SERVER_NAME'] = 'a.net'
183188
with app.test_request_context():
184-
dispatcher = make_url_fetcher(next_fetcher=dummy_fetcher)
189+
dispatcher = make_url_fetcher(next_fetcher=DummyFetcher)
185190
assert_app('http://a.net', '', '', '/')
186191
assert_app('http://a.net/', '', '', '/')
187192
assert_app('http://a.net/b/c/d?e', '', '', '/b/c/d', 'e')
@@ -195,7 +200,7 @@ def assert_dummy(url):
195200
# SERVER_NAME with a port number
196201
app.config['SERVER_NAME'] = 'a.net:8888'
197202
with app.test_request_context():
198-
dispatcher = make_url_fetcher(next_fetcher=dummy_fetcher)
203+
dispatcher = make_url_fetcher(next_fetcher=DummyFetcher)
199204
assert_app('http://a.net:8888', '', '', '/')
200205
assert_app('http://a.net:8888/', '', '', '/')
201206
assert_app('http://a.net:8888/b/c/d?e', '', '', '/b/c/d', 'e')
@@ -208,11 +213,9 @@ def assert_dummy(url):
208213

209214
@pytest.mark.parametrize('url', [
210215
'http://example.net/Unïĉodé/pass !',
211-
'http://example.net/Unïĉodé/pass !'.encode(),
212216
'http://example.net/foo%20bar/p%61ss%C2%A0!',
213-
b'http://example.net/foo%20bar/p%61ss%C2%A0!',
214217
])
215218
def test_funky_urls(url):
216219
with app.test_request_context(base_url='http://example.net/'):
217220
fetcher = make_url_fetcher()
218-
assert fetcher(url)['string'] == 'pass !'.encode()
221+
assert fetcher(url).read() == 'pass !'.encode()

0 commit comments

Comments
 (0)