Skip to content

Commit a22d9f0

Browse files
Jon Wayne Parrottdi
Jon Wayne Parrott
authored andcommitted
Refactor readme rendering logic (#3760)
* Refactor readme rendering logic * Add mock for readme render * Fix silly error
1 parent 432c672 commit a22d9f0

File tree

9 files changed

+142
-174
lines changed

9 files changed

+142
-174
lines changed

tests/functional/test_templates.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def test_templates_for_empty_titles():
4040
"format_classifiers": "warehouse.filters:format_classifiers",
4141
"format_tags": "warehouse.filters:format_tags",
4242
"json": "warehouse.filters:tojson",
43-
"readme": "warehouse.filters:readme",
43+
"camoify": "warehouse.filters:camoify",
4444
"shorten_number": "warehouse.filters:shorten_number",
4545
"urlparse": "warehouse.filters:urlparse",
4646
"contains_valid_uris": "warehouse.filters:contains_valid_uris",

tests/unit/packaging/test_views.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from pyramid.httpexceptions import HTTPMovedPermanently, HTTPNotFound
1616

1717
from warehouse.packaging import views
18+
from warehouse.utils import readme
1819

1920
from ...common.db.accounts import UserFactory
2021
from ...common.db.classifiers import ClassifierFactory
@@ -143,15 +144,19 @@ def test_normalizing_version_redirects(self, db_request):
143144
pretend.call(name=release.project.name, version=release.version),
144145
]
145146

146-
def test_detail_renders(self, db_request):
147+
def test_detail_renders(self, monkeypatch, db_request):
147148
users = [
148149
UserFactory.create(),
149150
UserFactory.create(),
150151
UserFactory.create(),
151152
]
152153
project = ProjectFactory.create()
153154
releases = [
154-
ReleaseFactory.create(project=project, version=v)
155+
ReleaseFactory.create(
156+
project=project,
157+
version=v,
158+
description="unrendered description",
159+
description_content_type="text/plain")
155160
for v in ["1.0", "2.0", "3.0", "4.0.dev0"]
156161
]
157162
files = [
@@ -174,12 +179,18 @@ def test_detail_renders(self, db_request):
174179
role_name="another role",
175180
)
176181

182+
# patch the readme rendering logic.
183+
render_description = pretend.call_recorder(
184+
lambda raw, content_type: "rendered description")
185+
monkeypatch.setattr(readme, "render", render_description)
186+
177187
result = views.release_detail(releases[1], db_request)
178188

179189
assert result == {
180190
"project": project,
181191
"release": releases[1],
182192
"files": [files[1]],
193+
"description": "rendered description",
183194
"latest_version": project.latest_version,
184195
"all_versions": [
185196
(r.version, r.created, r.is_prerelease)
@@ -189,6 +200,10 @@ def test_detail_renders(self, db_request):
189200
"license": None
190201
}
191202

203+
assert render_description.calls == [
204+
pretend.call("unrendered description", "text/plain")
205+
]
206+
192207
def test_license_from_classifier(self, db_request):
193208
"""A license label is added when a license classifier exists."""
194209
other_classifier = ClassifierFactory.create(

tests/unit/test_filters.py

Lines changed: 9 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,9 @@
1414

1515
from functools import partial
1616

17-
import jinja2
1817
import packaging.version
1918
import pretend
2019
import pytest
21-
import readme_renderer.rst
2220

2321
from warehouse import filters
2422

@@ -42,134 +40,9 @@ def test_camo_url():
4240
)
4341

4442

45-
class TestReadmeRender:
46-
47-
def test_can_render_rst(self, monkeypatch):
48-
renderer = pretend.call_recorder(lambda raw: "rendered")
49-
monkeypatch.setattr(readme_renderer.rst, "render", renderer)
50-
51-
request = pretend.stub(
52-
registry=pretend.stub(
53-
settings={
54-
"camo.url": "https://camo.example.net/",
55-
"camo.key": "fake key",
56-
},
57-
),
58-
)
59-
camo_url = partial(filters._camo_url, request)
60-
request.camo_url = camo_url
61-
62-
ctx = {"request": request}
63-
64-
result = filters.readme(
65-
ctx, "raw thing", description_content_type="text/x-rst",
66-
)
67-
68-
assert result == jinja2.Markup("rendered")
69-
assert renderer.calls == [pretend.call('raw thing')]
70-
71-
def test_cant_render_rst(self, monkeypatch):
72-
rst_renderer = pretend.call_recorder(lambda raw: None)
73-
txt_renderer = pretend.call_recorder(lambda raw: "rendered<br>thing")
74-
monkeypatch.setattr(readme_renderer.rst, "render", rst_renderer)
75-
monkeypatch.setattr(readme_renderer.txt, "render", txt_renderer)
76-
77-
request = pretend.stub(
78-
registry=pretend.stub(
79-
settings={
80-
"camo.url": "https://camo.example.net/",
81-
"camo.key": "fake key",
82-
},
83-
),
84-
)
85-
camo_url = partial(filters._camo_url, request)
86-
request.camo_url = camo_url
87-
88-
ctx = {"request": request}
89-
90-
result = filters.readme(
91-
ctx, "raw thing", description_content_type="text/x-rst",
92-
)
93-
94-
assert result == jinja2.Markup("rendered<br>thing")
95-
assert rst_renderer.calls == [pretend.call('raw thing')]
96-
assert txt_renderer.calls == [pretend.call('raw thing')]
97-
98-
def test_can_render_plaintext(self, monkeypatch):
99-
renderer = pretend.call_recorder(lambda raw: "rendered")
100-
monkeypatch.setattr(readme_renderer.txt, "render", renderer)
101-
102-
request = pretend.stub(
103-
registry=pretend.stub(
104-
settings={
105-
"camo.url": "https://camo.example.net/",
106-
"camo.key": "fake key",
107-
},
108-
),
109-
)
110-
camo_url = partial(filters._camo_url, request)
111-
request.camo_url = camo_url
112-
113-
ctx = {"request": request}
114-
115-
result = filters.readme(
116-
ctx, "raw thing", description_content_type="text/plain",
117-
)
118-
119-
assert result == jinja2.Markup("rendered")
120-
assert renderer.calls == [pretend.call('raw thing')]
121-
122-
def test_can_render_markdown(self, monkeypatch):
123-
renderer = pretend.call_recorder(lambda raw: "rendered")
124-
monkeypatch.setattr(readme_renderer.markdown, "render", renderer)
125-
126-
request = pretend.stub(
127-
registry=pretend.stub(
128-
settings={
129-
"camo.url": "https://camo.example.net/",
130-
"camo.key": "fake key",
131-
},
132-
),
133-
)
134-
camo_url = partial(filters._camo_url, request)
135-
request.camo_url = camo_url
136-
137-
ctx = {"request": request}
138-
139-
result = filters.readme(
140-
ctx, "raw thing", description_content_type="text/markdown",
141-
)
142-
143-
assert result == jinja2.Markup("rendered")
144-
assert renderer.calls == [pretend.call('raw thing')]
145-
146-
def test_can_render_missing_content_type(self, monkeypatch):
147-
renderer = pretend.call_recorder(lambda raw: "rendered")
148-
monkeypatch.setattr(readme_renderer.rst, "render", renderer)
149-
150-
request = pretend.stub(
151-
registry=pretend.stub(
152-
settings={
153-
"camo.url": "https://camo.example.net/",
154-
"camo.key": "fake key",
155-
},
156-
),
157-
)
158-
camo_url = partial(filters._camo_url, request)
159-
request.camo_url = camo_url
160-
161-
ctx = {"request": request}
162-
163-
result = filters.readme(
164-
ctx, "raw thing", description_content_type=None,
165-
)
166-
167-
assert result == jinja2.Markup("rendered")
168-
assert renderer.calls == [pretend.call('raw thing')]
169-
170-
def test_renders_camo(self, monkeypatch):
43+
class TestCamoify:
44+
def test_camoify(self):
17145
html = "<img src=http://example.com/image.jpg>"
172-
monkeypatch.setattr(readme_renderer.rst, "render", lambda raw: html)
17346

17447
request = pretend.stub(
17548
registry=pretend.stub(
@@ -184,19 +57,17 @@ def test_renders_camo(self, monkeypatch):
18457

18558
ctx = {"request": request}
18659

187-
result = filters.readme(
188-
ctx, "raw thing", description_content_type="text/x-rst",
60+
result = filters.camoify(
61+
ctx, html
18962
)
19063

191-
assert result == jinja2.Markup(
64+
assert result == (
19265
'<img src="https://camo.example.net/'
19366
'b410d235a3d2fc44b50ccab827e531dece213062/'
194-
'687474703a2f2f6578616d706c652e636f6d2f696d6167652e6a7067">'
195-
)
67+
'687474703a2f2f6578616d706c652e636f6d2f696d6167652e6a7067">')
19668

197-
def test_renders_camo_no_src(self, monkeypatch):
69+
def test_camoify_no_src(self, monkeypatch):
19870
html = "<img>"
199-
monkeypatch.setattr(readme_renderer.rst, "render", lambda raw: html)
20071

20172
request = pretend.stub(
20273
registry=pretend.stub(
@@ -216,11 +87,9 @@ def test_renders_camo_no_src(self, monkeypatch):
21687
)
21788
monkeypatch.setattr(filters, "_camo_url", gen_camo_url)
21889

219-
result = filters.readme(
220-
ctx, "raw thing", description_content_type="text/x-rst",
221-
)
90+
result = filters.camoify(ctx, html)
22291

223-
assert result == jinja2.Markup("<img>")
92+
assert result == "<img>"
22493
assert gen_camo_url.calls == []
22594

22695

tests/unit/utils/test_readme.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License");
2+
# you may not use this file except in compliance with the License.
3+
# You may obtain a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS,
9+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
# See the License for the specific language governing permissions and
11+
# limitations under the License.
12+
13+
from warehouse.utils import readme
14+
15+
16+
def test_render_with_none():
17+
result = readme.render(None)
18+
assert result is None
19+
20+
21+
def test_can_render_rst():
22+
result = readme.render("raw thing", "text/x-rst")
23+
assert result == "<p>raw thing</p>\n"
24+
25+
26+
def test_cant_render_rst():
27+
result = readme.render("raw `<thing", "text/x-rst")
28+
assert result == "raw `&lt;thing"
29+
30+
31+
def test_can_render_plaintext():
32+
result = readme.render("raw thing", "text/plain")
33+
assert result == "raw thing"
34+
35+
36+
def test_can_render_markdown():
37+
result = readme.render("raw thing", "text/markdown")
38+
assert result == "<p>raw thing</p>\n"
39+
40+
41+
def test_can_render_missing_content_type():
42+
result = readme.render("raw thing")
43+
assert result == "<p>raw thing</p>\n"
44+
45+
46+
def test_renderer_version():
47+
assert readme.renderer_version() is not None

warehouse/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ def configure(settings=None):
327327
)
328328
filters.setdefault("format_tags", "warehouse.filters:format_tags")
329329
filters.setdefault("json", "warehouse.filters:tojson")
330-
filters.setdefault("readme", "warehouse.filters:readme")
330+
filters.setdefault("camoify", "warehouse.filters:camoify")
331331
filters.setdefault("shorten_number", "warehouse.filters:shorten_number")
332332
filters.setdefault("urlparse", "warehouse.filters:urlparse")
333333
filters.setdefault(

warehouse/filters.py

Lines changed: 4 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
# limitations under the License.
1212

1313
import binascii
14-
import cgi
1514
import collections
1615
import enum
1716
import hmac
@@ -25,21 +24,11 @@
2524
import jinja2
2625

2726
import packaging.version
28-
import readme_renderer.markdown
29-
import readme_renderer.rst
30-
import readme_renderer.txt
3127

3228
from pyramid.threadlocal import get_current_request
3329

3430
from warehouse.utils.http import is_valid_uri
3531

36-
_renderers = {
37-
'': readme_renderer.rst, # Default if description_content_type is None
38-
'text/plain': readme_renderer.txt,
39-
'text/x-rst': readme_renderer.rst,
40-
'text/markdown': readme_renderer.markdown,
41-
}
42-
4332

4433
class PackageType(enum.Enum):
4534
bdist_dmg = "OSX Disk Image"
@@ -73,27 +62,14 @@ def _camo_url(request, url):
7362

7463

7564
@jinja2.contextfilter
76-
def readme(ctx, value, *, description_content_type):
65+
def camoify(ctx, value):
7766
request = ctx.get("request") or get_current_request()
7867

79-
content_type, parameters = cgi.parse_header(description_content_type or '')
80-
81-
# Get the appropriate renderer
82-
renderer = _renderers[content_type]
83-
84-
# Actually render the given value, this will not only render the value, but
85-
# also ensure that it's had any disallowed markup removed.
86-
rendered = renderer.render(value, **parameters)
87-
88-
# If the content was not rendered, we'll render as plaintext instead
89-
if rendered is None:
90-
rendered = readme_renderer.txt.render(value)
91-
9268
# Parse the rendered output and replace any inline images that don't point
9369
# to HTTPS with camouflaged images.
9470
tree_builder = html5lib.treebuilders.getTreeBuilder("dom")
9571
parser = html5lib.html5parser.HTMLParser(tree=tree_builder)
96-
dom = parser.parse(rendered)
72+
dom = parser.parse(value)
9773

9874
for element in dom.getElementsByTagName("img"):
9975
src = element.getAttribute("src")
@@ -102,9 +78,9 @@ def readme(ctx, value, *, description_content_type):
10278

10379
tree_walker = html5lib.treewalkers.getTreeWalker("dom")
10480
html_serializer = html5lib.serializer.HTMLSerializer()
105-
rendered = "".join(html_serializer.serialize(tree_walker(dom)))
81+
camoed = "".join(html_serializer.serialize(tree_walker(dom)))
10682

107-
return jinja2.Markup(rendered)
83+
return camoed
10884

10985

11086
_SI_SYMBOLS = ["k", "M", "G", "T", "P", "E", "Z", "Y"]

0 commit comments

Comments
 (0)