Repository: pypi/warehouse

Issue:
Implement redirects from the old PyPI URLs to the new Project URLs



Discussion:

Resolving patch:
diff --git a/warehouse/config.py b/warehouse/config.py
--- a/warehouse/config.py
+++ b/warehouse/config.py
@@ -103,6 +103,9 @@ def configure(settings=None):
     # Allow the packaging app to register any services it has.
     config.include(".packaging")
 
+    # Configure redirection support
+    config.include(".redirects")
+
     # Register all our URL routes for Warehouse.
     config.include(".routes")
 
diff --git a/warehouse/redirects.py b/warehouse/redirects.py
new file mode 100644
--- /dev/null
+++ b/warehouse/redirects.py
@@ -0,0 +1,30 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from pyramid.httpexceptions import HTTPMovedPermanently
+
+
+def redirect_view_factory(target, redirect=HTTPMovedPermanently):
+    def redirect_view(request):
+        return redirect(target.format(_request=request, **request.matchdict))
+    return redirect_view
+
+
+def add_redirect(config, source, target, **kw):
+    route_name = "warehouse.redirects." + source
+
+    config.add_route(route_name, source)
+    config.add_view(redirect_view_factory(target, **kw), route_name=route_name)
+
+
+def includeme(config):
+    config.add_directive("add_redirect", add_redirect, action_wrap=False)
diff --git a/warehouse/routes.py b/warehouse/routes.py
--- a/warehouse/routes.py
+++ b/warehouse/routes.py
@@ -45,3 +45,10 @@ def includeme(config):
         factory="warehouse.packaging.models:ProjectFactory",
         traverse="/{name}/",
     )
+
+    # Legacy Redirects
+    config.add_redirect("/pypi/{name}/", "/project/{name}/")
+    config.add_redirect(
+        "/pypi/{name}/{version}/",
+        "/project/{name}/{version}/",
+    )


Test patch:
diff --git a/tests/test_config.py b/tests/test_config.py
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -143,6 +143,7 @@ def __init__(self):
         pretend.call(".csrf"),
         pretend.call(".accounts"),
         pretend.call(".packaging"),
+        pretend.call(".redirects"),
         pretend.call(".routes"),
     ]
     assert configurator_obj.add_jinja2_renderer.calls == [
diff --git a/tests/test_redirects.py b/tests/test_redirects.py
new file mode 100644
--- /dev/null
+++ b/tests/test_redirects.py
@@ -0,0 +1,67 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pretend
+
+from pyramid.httpexceptions import HTTPMovedPermanently
+
+from warehouse import redirects
+
+
+def test_redirect_view():
+    target = "/{wat}/{_request.method}"
+    view = redirects.redirect_view_factory(target)
+
+    request = pretend.stub(method="GET", matchdict={"wat": "the-thing"})
+    resp = view(request)
+
+    assert isinstance(resp, HTTPMovedPermanently)
+    assert resp.headers["Location"] == "/the-thing/GET"
+
+
+def test_add_redirect(monkeypatch):
+    rview = pretend.stub()
+    rview_factory = pretend.call_recorder(lambda target, redirect: rview)
+    monkeypatch.setattr(redirects, "redirect_view_factory", rview_factory)
+
+    config = pretend.stub(
+        add_route=pretend.call_recorder(lambda name, route: None),
+        add_view=pretend.call_recorder(lambda view, route_name: None),
+    )
+
+    source = "/the/{thing}/"
+    target = "/other/{thing}/"
+    redirect = pretend.stub()
+
+    redirects.add_redirect(config, source, target, redirect=redirect)
+
+    assert config.add_route.calls == [
+        pretend.call("warehouse.redirects." + source, source),
+    ]
+    assert config.add_view.calls == [
+        pretend.call(rview, route_name="warehouse.redirects." + source),
+    ]
+    assert rview_factory.calls == [pretend.call(target, redirect=redirect)]
+
+
+def test_includeme():
+    config = pretend.stub(
+        add_directive=pretend.call_recorder(lambda n, fn, action_wrap: None),
+    )
+    redirects.includeme(config)
+    assert config.add_directive.calls == [
+        pretend.call(
+            "add_redirect",
+            redirects.add_redirect,
+            action_wrap=False,
+        ),
+    ]
diff --git a/tests/test_routes.py b/tests/test_routes.py
--- a/tests/test_routes.py
+++ b/tests/test_routes.py
@@ -22,6 +22,11 @@ class FakeConfig:
         def add_route(*args, **kwargs):
             pass
 
+        @staticmethod
+        @pretend.call_recorder
+        def add_redirect(*args, **kwargs):
+            pass
+
     config = FakeConfig()
     includeme(config)
 
@@ -55,3 +60,8 @@ def add_route(*args, **kwargs):
             traverse="/{name}/",
         ),
     ]
+
+    assert config.add_redirect.calls == [
+        pretend.call("/pypi/{name}/", "/project/{name}/"),
+        pretend.call("/pypi/{name}/{version}/", "/project/{name}/{version}/"),
+    ]