Skip to content

Commit 04d0cee

Browse files
authored
Merge pull request #1065 from jobselko/close_1054
[PULP-1082] Add data-requires-python to Simple HTML API
2 parents 0ea858a + b307b21 commit 04d0cee

File tree

5 files changed

+105
-37
lines changed

5 files changed

+105
-37
lines changed

CHANGES/1054.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added data-requires-python to Simple HTML API.

pulp_python/app/utils.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
</html>
4848
"""
4949

50-
# TODO in the future: data-requires-python (PEP 503)
50+
# TODO in the future: data-yanked (not implemented yet because it is mutable)
5151
simple_detail_template = """<!DOCTYPE html>
5252
<html>
5353
<head>
@@ -58,6 +58,7 @@
5858
<h1>Links for {{ project_name }}</h1>
5959
{%- for pkg in project_packages %}
6060
<a href="{{ pkg.url }}#sha256={{ pkg.sha256 }}" rel="internal"
61+
{%- if pkg.requires_python %} data-requires-python="{{ pkg.requires_python }}" {%- endif %}
6162
{%- if pkg.metadata_sha256 %} data-dist-info-metadata="sha256={{ pkg.metadata_sha256 }}" data-core-metadata="sha256={{ pkg.metadata_sha256 }}"
6263
{%- endif %} {% if pkg.provenance -%}
6364
data-provenance="{{ pkg.provenance }}"{% endif %}>{{ pkg.filename }}</a><br/>
@@ -501,7 +502,7 @@ def write_simple_index(project_names, streamed=False):
501502

502503
def write_simple_detail(project_name, project_packages, streamed=False):
503504
"""Writes the simple detail page of a package."""
504-
detail = Template(simple_detail_template)
505+
detail = Template(simple_detail_template, autoescape=True)
505506
context = {
506507
"SIMPLE_API_VERSION": SIMPLE_API_VERSION,
507508
"project_name": project_name,

pulp_python/tests/functional/api/test_pypi_simple_api.py

Lines changed: 38 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,23 @@
55

66
from pulp_python.tests.functional.constants import (
77
PYPI_SERIAL_CONSTANT,
8-
PYTHON_EGG_FILENAME,
9-
PYTHON_EGG_SHA256,
10-
PYTHON_EGG_URL,
118
PYTHON_SM_FIXTURE_CHECKSUMS,
129
PYTHON_SM_FIXTURE_RELEASES,
1310
PYTHON_SM_PROJECT_SPECIFIER,
14-
PYTHON_WHEEL_FILENAME,
15-
PYTHON_WHEEL_METADATA_SHA256,
16-
PYTHON_WHEEL_SHA256,
17-
PYTHON_WHEEL_URL,
18-
PYTHON_XS_FIXTURE_CHECKSUMS,
11+
TWINE_EGG_FILENAME,
12+
TWINE_EGG_REQUIRES_PYTHON,
13+
TWINE_EGG_SHA256,
14+
TWINE_EGG_SIZE,
15+
TWINE_EGG_URL,
16+
TWINE_FIXTURE_CHECKSUMS,
17+
TWINE_FIXTURE_METADATA_SHA256,
18+
TWINE_FIXTURE_REQUIRES_PYTHON,
19+
TWINE_WHEEL_FILENAME,
20+
TWINE_WHEEL_METADATA_SHA256,
21+
TWINE_WHEEL_REQUIRES_PYTHON,
22+
TWINE_WHEEL_SHA256,
23+
TWINE_WHEEL_SIZE,
24+
TWINE_WHEEL_URL,
1925
)
2026
from pulp_python.tests.functional.utils import ensure_simple
2127

@@ -55,30 +61,27 @@ def test_simple_html_detail_api(
5561
python_distribution_factory,
5662
python_repo_factory,
5763
):
58-
content_1 = python_content_factory(PYTHON_WHEEL_FILENAME, url=PYTHON_WHEEL_URL)
59-
content_2 = python_content_factory(PYTHON_EGG_FILENAME, url=PYTHON_EGG_URL)
64+
content_1 = python_content_factory(TWINE_WHEEL_FILENAME, url=TWINE_WHEEL_URL)
65+
content_2 = python_content_factory(TWINE_EGG_FILENAME, url=TWINE_EGG_URL)
6066
body = {"add_content_units": [content_1.pulp_href, content_2.pulp_href]}
6167

6268
repo = python_repo_factory()
6369
monitor_task(python_bindings.RepositoriesPythonApi.modify(repo.pulp_href, body).task)
6470
distro = python_distribution_factory(repository=repo)
6571

66-
url = f'{urljoin(distro.base_url, "simple/")}shelf-reader'
72+
url = f'{urljoin(distro.base_url, "simple/")}twine'
6773
headers = {"Accept": PYPI_SIMPLE_V1_HTML}
6874

6975
response = requests.get(url, headers=headers)
7076
assert response.headers["Content-Type"] == PYPI_SIMPLE_V1_HTML
7177
assert response.headers["X-PyPI-Last-Serial"] == str(PYPI_SERIAL_CONSTANT)
7278

73-
metadata_sha_digests = {
74-
PYTHON_WHEEL_FILENAME: PYTHON_WHEEL_METADATA_SHA256,
75-
PYTHON_EGG_FILENAME: None, # egg files should not have metadata
76-
}
7779
proper, msgs = ensure_simple(
7880
urljoin(distro.base_url, "simple/"),
79-
{"shelf-reader": [PYTHON_WHEEL_FILENAME, PYTHON_EGG_FILENAME]},
80-
sha_digests=PYTHON_XS_FIXTURE_CHECKSUMS,
81-
metadata_sha_digests=metadata_sha_digests,
81+
{"twine": [TWINE_WHEEL_FILENAME, TWINE_EGG_FILENAME]},
82+
sha_digests=TWINE_FIXTURE_CHECKSUMS,
83+
metadata_sha_digests=TWINE_FIXTURE_METADATA_SHA256,
84+
requires_python=TWINE_FIXTURE_REQUIRES_PYTHON,
8285
)
8386
assert proper, f"Simple API validation failed: {msgs}"
8487

@@ -114,15 +117,15 @@ def test_simple_json_detail_api(
114117
python_distribution_factory,
115118
python_repo_factory,
116119
):
117-
content_1 = python_content_factory(PYTHON_WHEEL_FILENAME, url=PYTHON_WHEEL_URL)
118-
content_2 = python_content_factory(PYTHON_EGG_FILENAME, url=PYTHON_EGG_URL)
120+
content_1 = python_content_factory(TWINE_WHEEL_FILENAME, url=TWINE_WHEEL_URL)
121+
content_2 = python_content_factory(TWINE_EGG_FILENAME, url=TWINE_EGG_URL)
119122
body = {"add_content_units": [content_1.pulp_href, content_2.pulp_href]}
120123

121124
repo = python_repo_factory()
122125
monitor_task(python_bindings.RepositoriesPythonApi.modify(repo.pulp_href, body).task)
123126
distro = python_distribution_factory(repository=repo)
124127

125-
url = f'{urljoin(distro.base_url, "simple/")}shelf-reader'
128+
url = f'{urljoin(distro.base_url, "simple/")}twine'
126129
headers = {"Accept": PYPI_SIMPLE_V1_JSON}
127130

128131
response = requests.get(url, headers=headers)
@@ -131,29 +134,31 @@ def test_simple_json_detail_api(
131134

132135
data = response.json()
133136
assert data["meta"] == {"api-version": API_VERSION, "_last-serial": PYPI_SERIAL_CONSTANT}
134-
assert data["name"] == "shelf-reader"
137+
assert data["name"] == "twine"
135138
assert data["files"]
136-
assert data["versions"] == ["0.1"]
139+
assert data["versions"] == ["5.1.0"]
137140

138141
# Check data of a wheel
139-
file_whl = next((i for i in data["files"] if i["filename"] == PYTHON_WHEEL_FILENAME), None)
142+
file_whl = next((i for i in data["files"] if i["filename"] == TWINE_WHEEL_FILENAME), None)
140143
assert file_whl is not None, "wheel file not found"
141144
assert file_whl["url"]
142-
assert file_whl["hashes"] == {"sha256": PYTHON_WHEEL_SHA256}
143-
assert file_whl["requires-python"] is None
144-
assert file_whl["data-dist-info-metadata"] == {"sha256": PYTHON_WHEEL_METADATA_SHA256}
145-
assert file_whl["core-metadata"] == {"sha256": PYTHON_WHEEL_METADATA_SHA256}
146-
assert file_whl["size"] == 22455
145+
assert file_whl["hashes"] == {"sha256": TWINE_WHEEL_SHA256}
146+
assert file_whl["requires-python"] == TWINE_WHEEL_REQUIRES_PYTHON
147+
assert file_whl["data-dist-info-metadata"] == {"sha256": TWINE_WHEEL_METADATA_SHA256}
148+
assert file_whl["core-metadata"] == {"sha256": TWINE_WHEEL_METADATA_SHA256}
149+
assert file_whl["size"] == TWINE_WHEEL_SIZE
147150
assert file_whl["upload-time"] is not None
151+
assert file_whl["provenance"] is None
152+
148153
# Check data of a tarball
149-
file_tar = next((i for i in data["files"] if i["filename"] == PYTHON_EGG_FILENAME), None)
154+
file_tar = next((i for i in data["files"] if i["filename"] == TWINE_EGG_FILENAME), None)
150155
assert file_tar is not None, "tar file not found"
151156
assert file_tar["url"]
152-
assert file_tar["hashes"] == {"sha256": PYTHON_EGG_SHA256}
153-
assert file_tar["requires-python"] is None
157+
assert file_tar["hashes"] == {"sha256": TWINE_EGG_SHA256}
158+
assert file_tar["requires-python"] == TWINE_EGG_REQUIRES_PYTHON
154159
assert file_tar["data-dist-info-metadata"] is False
155160
assert file_tar["core-metadata"] is False
156-
assert file_tar["size"] == 19097
161+
assert file_tar["size"] == TWINE_EGG_SIZE
157162
assert file_tar["upload-time"] is not None
158163
assert file_tar["provenance"] is None
159164

pulp_python/tests/functional/constants.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,32 @@
226226
# maybe add description, license is long for this one
227227
}
228228

229+
# twine pkg data for PyPI Simple API
230+
TWINE_EGG_FILENAME = "twine-5.1.0.tar.gz"
231+
TWINE_EGG_URL = urljoin(urljoin(PYTHON_FIXTURES_URL, "packages/"), TWINE_EGG_FILENAME)
232+
TWINE_EGG_SHA256 = "4d74770c88c4fcaf8134d2a6a9d863e40f08255ff7d8e2acb3cbbd57d25f6e9d"
233+
TWINE_EGG_SIZE = 224997
234+
TWINE_EGG_REQUIRES_PYTHON = ">=3.8"
235+
236+
TWINE_WHEEL_FILENAME = "twine-5.1.0-py3-none-any.whl"
237+
TWINE_WHEEL_URL = urljoin(urljoin(PYTHON_FIXTURES_URL, "packages/"), TWINE_WHEEL_FILENAME)
238+
TWINE_WHEEL_SHA256 = "fe1d814395bfe50cfbe27783cb74efe93abeac3f66deaeb6c8390e4e92bacb43"
239+
TWINE_WHEEL_SIZE = 38563
240+
TWINE_WHEEL_REQUIRES_PYTHON = ">=3.8"
241+
TWINE_WHEEL_METADATA_SHA256 = "0ac5cf457bd47512b3477949ff6274cc2258414f3e1f136e049585aac92e4ddb"
242+
243+
TWINE_FIXTURE_CHECKSUMS = {
244+
TWINE_EGG_FILENAME: TWINE_EGG_SHA256,
245+
TWINE_WHEEL_FILENAME: TWINE_WHEEL_SHA256,
246+
}
247+
TWINE_FIXTURE_METADATA_SHA256 = {
248+
TWINE_WHEEL_FILENAME: TWINE_WHEEL_METADATA_SHA256,
249+
TWINE_EGG_FILENAME: None, # egg files should not have metadata
250+
}
251+
TWINE_FIXTURE_REQUIRES_PYTHON = {
252+
TWINE_WHEEL_FILENAME: TWINE_EGG_REQUIRES_PYTHON,
253+
TWINE_EGG_FILENAME: TWINE_WHEEL_REQUIRES_PYTHON,
254+
}
229255

230256
# Current tests use PYTHON_FIXTURES_URL with an 'S', remove after adding api tests
231257
PYTHON_FIXTURE_URL = urljoin(PULP_FIXTURES_BASE_URL, "python-pypi/")

pulp_python/tests/functional/utils.py

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,36 @@ def _validate_metadata_sha_digest(link, filename, metadata_sha_digests):
2525
return msgs
2626

2727

28-
def ensure_simple(simple_url, packages, sha_digests=None, metadata_sha_digests=None):
28+
def _validate_requires_python(link, filename, requires_python, page_content):
29+
"""
30+
Validate data-requires-python attribute for a release link.
31+
"""
32+
expected_requires_python = requires_python.get(filename) if requires_python else None
33+
attr_value = link.get("data-requires-python")
34+
35+
msgs = ""
36+
if attr_value != expected_requires_python:
37+
if expected_requires_python:
38+
msgs += (
39+
f"\nFile {filename} has incorrect data-requires-python: "
40+
f"expected '{expected_requires_python}', got '{attr_value}'"
41+
)
42+
else:
43+
msgs += f"\nFile {filename} should not have data-requires-python but has '{attr_value}'"
44+
45+
# Check HTML escaping
46+
if expected_requires_python and any(char in expected_requires_python for char in [">", "<"]):
47+
escaped_value = expected_requires_python.replace(">", "&gt;").replace("<", "&lt;")
48+
escaped_attr = f'data-requires-python="{escaped_value}"'
49+
if escaped_attr not in page_content:
50+
msgs += f"\nFile {filename} has unescaped < or > in data-requires-python attribute"
51+
52+
return msgs
53+
54+
55+
def ensure_simple(
56+
simple_url, packages, sha_digests=None, metadata_sha_digests=None, requires_python=None
57+
):
2958
"""
3059
Tests that the simple api at `url` matches the packages supplied.
3160
`packages`: dictionary of form {package_name: [release_filenames]}
@@ -40,7 +69,8 @@ def ensure_simple(simple_url, packages, sha_digests=None, metadata_sha_digests=N
4069

4170
def explore_links(page_url, page_name, links_found, msgs):
4271
legit_found_links = []
43-
page = html.fromstring(requests.get(page_url).text)
72+
page_content = requests.get(page_url).text
73+
page = html.fromstring(page_content)
4474
page_links = page.xpath("/html/body/a")
4575
for link in page_links:
4676
if link.text in links_found:
@@ -52,6 +82,11 @@ def explore_links(page_url, page_name, links_found, msgs):
5282
# Check metadata SHA digest if provided
5383
if metadata_sha_digests and page_name == "release":
5484
msgs += _validate_metadata_sha_digest(link, link.text, metadata_sha_digests)
85+
# Check requires-python if provided
86+
if requires_python and page_name == "release":
87+
msgs += _validate_requires_python(
88+
link, link.text, requires_python, page_content
89+
)
5590
else:
5691
msgs += f"\nFound {page_name} link without href {link.text}"
5792
else:

0 commit comments

Comments
 (0)