Skip to content

Commit d3ed6e0

Browse files
cofiemewdurbin
andauthored
Implement Alternate Repository Location for PEP 708 (pypi#15716)
* initial attempt at adding alternate repository location details * implement per-project alternate locations metadata * starting to add tests * starting to add tests * added tests Fixed rendering for detail.html. Moved api mimetypes to const vars. Check delete confirmation name matches. * updated translations * satisfy test coverage * update translations * update translations * register cache and purge keys for AlternateRepository objects * change db migration down revision to most recent migration This allows the migrations to run. * update test after adding alternate repository cache and purge key * increment api version to 1.2 * add url the response was fetched from * change db migration down revision to most recent migration This allows the migrations to run. * name is already normalized * update translations * match functionality between JSON and HTML simple API - route_path -> route_url to get full URL rather than path - move self reference to the _simple_detail helper * update migration * remove self-reference from Simple HTML and JSON The PEP reads as though they can be implied "When using alternate locations, clients MUST implicitly assume that the url the response was fetched from was included in the list." * add a callout in project management settings around Alternate Locations * translations --------- Co-authored-by: Ee Durbin <[email protected]>
1 parent 453244f commit d3ed6e0

File tree

17 files changed

+1101
-101
lines changed

17 files changed

+1101
-101
lines changed

tests/common/db/packaging.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
from warehouse.observations.models import ObservationKind
2222
from warehouse.packaging.models import (
23+
AlternateRepository,
2324
Dependency,
2425
DependencyKind,
2526
Description,
@@ -200,3 +201,13 @@ class Meta:
200201
)
201202
name = factory.Faker("pystr", max_chars=12)
202203
prohibited_by = factory.SubFactory(UserFactory)
204+
205+
206+
class AlternateRepositoryFactory(WarehouseFactory):
207+
class Meta:
208+
model = AlternateRepository
209+
210+
name = factory.Faker("word")
211+
url = factory.Faker("uri")
212+
description = factory.Faker("text")
213+
project = factory.SubFactory(ProjectFactory)

tests/unit/api/test_simple.py

Lines changed: 51 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@
1818
from pyramid.testing import DummyRequest
1919

2020
from warehouse.api import simple
21-
from warehouse.packaging.utils import API_VERSION
21+
from warehouse.packaging.utils import API_VERSION, _valid_simple_detail_context
2222

2323
from ...common.db.accounts import UserFactory
2424
from ...common.db.packaging import (
25+
AlternateRepositoryFactory,
2526
FileFactory,
2627
JournalEntryFactory,
2728
ProjectFactory,
@@ -48,29 +49,30 @@ def test_defaults_text_html(self, header):
4849
default to text/html.
4950
"""
5051
request = DummyRequest(accept=header)
51-
assert simple._select_content_type(request) == "text/html"
52+
assert simple._select_content_type(request) == simple.MIME_TEXT_HTML
5253

5354
@pytest.mark.parametrize(
5455
("header", "expected"),
5556
[
56-
("text/html", "text/html"),
57+
(simple.MIME_TEXT_HTML, simple.MIME_TEXT_HTML),
5758
(
58-
"application/vnd.pypi.simple.v1+html",
59-
"application/vnd.pypi.simple.v1+html",
59+
simple.MIME_PYPI_SIMPLE_V1_HTML,
60+
simple.MIME_PYPI_SIMPLE_V1_HTML,
6061
),
6162
(
62-
"application/vnd.pypi.simple.v1+json",
63-
"application/vnd.pypi.simple.v1+json",
63+
simple.MIME_PYPI_SIMPLE_V1_JSON,
64+
simple.MIME_PYPI_SIMPLE_V1_JSON,
6465
),
6566
(
66-
"text/html, application/vnd.pypi.simple.v1+html, "
67-
"application/vnd.pypi.simple.v1+json",
68-
"text/html",
67+
f"{simple.MIME_TEXT_HTML}, {simple.MIME_PYPI_SIMPLE_V1_HTML}, "
68+
f"{simple.MIME_PYPI_SIMPLE_V1_JSON}",
69+
simple.MIME_TEXT_HTML,
6970
),
7071
(
71-
"text/html;q=0.01, application/vnd.pypi.simple.v1+html;q=0.2, "
72-
"application/vnd.pypi.simple.v1+json",
73-
"application/vnd.pypi.simple.v1+json",
72+
f"{simple.MIME_TEXT_HTML};q=0.01, "
73+
f"{simple.MIME_PYPI_SIMPLE_V1_HTML};q=0.2, "
74+
f"{simple.MIME_PYPI_SIMPLE_V1_JSON}",
75+
simple.MIME_PYPI_SIMPLE_V1_JSON,
7476
),
7577
],
7678
)
@@ -80,9 +82,9 @@ def test_selects(self, header, expected):
8082

8183

8284
CONTENT_TYPE_PARAMS = [
83-
("text/html", None),
84-
("application/vnd.pypi.simple.v1+html", None),
85-
("application/vnd.pypi.simple.v1+json", "json"),
85+
(simple.MIME_TEXT_HTML, None),
86+
(simple.MIME_PYPI_SIMPLE_V1_HTML, None),
87+
(simple.MIME_PYPI_SIMPLE_V1_JSON, "json"),
8688
]
8789

8890

@@ -211,12 +213,15 @@ def test_no_files_no_serial(self, db_request, content_type, renderer_override):
211213
user = UserFactory.create()
212214
JournalEntryFactory.create(submitted_by=user)
213215

214-
assert simple.simple_detail(project, db_request) == {
216+
context = {
215217
"meta": {"_last-serial": 0, "api-version": API_VERSION},
216218
"name": project.normalized_name,
217219
"files": [],
218220
"versions": [],
221+
"alternate-locations": [],
219222
}
223+
context = _update_context(context, content_type, renderer_override)
224+
assert simple.simple_detail(project, db_request) == context
220225

221226
assert db_request.response.headers["X-PyPI-Last-Serial"] == "0"
222227
assert db_request.response.content_type == content_type
@@ -235,13 +240,20 @@ def test_no_files_with_serial(self, db_request, content_type, renderer_override)
235240
db_request.matchdict["name"] = project.normalized_name
236241
user = UserFactory.create()
237242
je = JournalEntryFactory.create(name=project.name, submitted_by=user)
243+
als = [
244+
AlternateRepositoryFactory.create(project=project),
245+
AlternateRepositoryFactory.create(project=project),
246+
]
238247

239-
assert simple.simple_detail(project, db_request) == {
248+
context = {
240249
"meta": {"_last-serial": je.id, "api-version": API_VERSION},
241250
"name": project.normalized_name,
242251
"files": [],
243252
"versions": [],
253+
"alternate-locations": sorted(al.url for al in als),
244254
}
255+
context = _update_context(context, content_type, renderer_override)
256+
assert simple.simple_detail(project, db_request) == context
245257

246258
assert db_request.response.headers["X-PyPI-Last-Serial"] == str(je.id)
247259
assert db_request.response.content_type == content_type
@@ -271,7 +283,7 @@ def test_with_files_no_serial(self, db_request, content_type, renderer_override)
271283
user = UserFactory.create()
272284
JournalEntryFactory.create(submitted_by=user)
273285

274-
assert simple.simple_detail(project, db_request) == {
286+
context = {
275287
"meta": {"_last-serial": 0, "api-version": API_VERSION},
276288
"name": project.normalized_name,
277289
"versions": release_versions,
@@ -289,7 +301,10 @@ def test_with_files_no_serial(self, db_request, content_type, renderer_override)
289301
}
290302
for f in files
291303
],
304+
"alternate-locations": [],
292305
}
306+
context = _update_context(context, content_type, renderer_override)
307+
assert simple.simple_detail(project, db_request) == context
293308

294309
assert db_request.response.headers["X-PyPI-Last-Serial"] == "0"
295310
assert db_request.response.content_type == content_type
@@ -319,7 +334,7 @@ def test_with_files_with_serial(self, db_request, content_type, renderer_overrid
319334
user = UserFactory.create()
320335
je = JournalEntryFactory.create(name=project.name, submitted_by=user)
321336

322-
assert simple.simple_detail(project, db_request) == {
337+
context = {
323338
"meta": {"_last-serial": je.id, "api-version": API_VERSION},
324339
"name": project.normalized_name,
325340
"versions": release_versions,
@@ -337,7 +352,10 @@ def test_with_files_with_serial(self, db_request, content_type, renderer_overrid
337352
}
338353
for f in files
339354
],
355+
"alternate-locations": [],
340356
}
357+
context = _update_context(context, content_type, renderer_override)
358+
assert simple.simple_detail(project, db_request) == context
341359

342360
assert db_request.response.headers["X-PyPI-Last-Serial"] == str(je.id)
343361
assert db_request.response.content_type == content_type
@@ -404,7 +422,7 @@ def test_with_files_with_version_multi_digit(
404422
user = UserFactory.create()
405423
je = JournalEntryFactory.create(name=project.name, submitted_by=user)
406424

407-
assert simple.simple_detail(project, db_request) == {
425+
context = {
408426
"meta": {"_last-serial": je.id, "api-version": API_VERSION},
409427
"name": project.normalized_name,
410428
"versions": release_versions,
@@ -430,7 +448,10 @@ def test_with_files_with_version_multi_digit(
430448
}
431449
for f in files
432450
],
451+
"alternate-locations": [],
433452
}
453+
context = _update_context(context, content_type, renderer_override)
454+
assert simple.simple_detail(project, db_request) == context
434455

435456
assert db_request.response.headers["X-PyPI-Last-Serial"] == str(je.id)
436457
assert db_request.response.content_type == content_type
@@ -439,6 +460,15 @@ def test_with_files_with_version_multi_digit(
439460
if renderer_override is not None:
440461
assert db_request.override_renderer == renderer_override
441462

463+
464+
def _update_context(context, content_type, renderer_override):
465+
if renderer_override != "json" or content_type in [
466+
simple.MIME_TEXT_HTML,
467+
simple.MIME_PYPI_SIMPLE_V1_HTML,
468+
]:
469+
return _valid_simple_detail_context(context)
470+
return context
471+
442472
def test_with_files_quarantined_omitted_from_index(self, db_request):
443473
db_request.accept = "text/html"
444474
project = ProjectFactory.create(lifecycle_status="quarantine-enter")

0 commit comments

Comments
 (0)