diff --git a/tests/unit/legacy/api/test_json.py b/tests/unit/legacy/api/test_json.py index 3a4ddbd1e6d6..04e47e99b066 100644 --- a/tests/unit/legacy/api/test_json.py +++ b/tests/unit/legacy/api/test_json.py @@ -10,6 +10,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import json as _json + import pretend from pyramid.httpexceptions import HTTPMovedPermanently, HTTPNotFound @@ -299,7 +301,7 @@ def test_detail_renders(self, pyramid_config, db_request, db_session): db_request.route_url = pretend.call_recorder(lambda *args, **kw: url) - result = json.json_release(releases[3], db_request) + json.json_release(releases[3], db_request) assert set(db_request.route_url.calls) == { pretend.call("packaging.file", path=files[0].path), @@ -315,86 +317,115 @@ def test_detail_renders(self, pyramid_config, db_request, db_session): _assert_has_cors_headers(db_request.response.headers) assert db_request.response.headers["X-PyPI-Last-Serial"] == str(je.id) - assert result == { - "info": { - "author": None, - "author_email": None, - "bugtrack_url": None, - "classifiers": [], - "description_content_type": description_content_type, - "description": releases[-1].description.raw, - "docs_url": "/the/fake/url/", - "download_url": None, - "downloads": {"last_day": -1, "last_week": -1, "last_month": -1}, - "home_page": None, - "keywords": None, - "license": None, - "maintainer": None, - "maintainer_email": None, - "name": project.name, - "package_url": "/the/fake/url/", - "platform": None, - "project_url": "/the/fake/url/", - "project_urls": expected_urls, - "release_url": "/the/fake/url/", - "requires_dist": None, - "requires_python": None, - "summary": None, - "yanked": False, - "yanked_reason": None, - "version": "3.0", - }, - "releases": { - "0.1": [], - "1.0": [ - { - "comment_text": None, - "downloads": -1, - "filename": files[0].filename, - "has_sig": True, - "md5_digest": files[0].md5_digest, - "digests": { - "md5": files[0].md5_digest, - "sha256": files[0].sha256_digest, - }, - "packagetype": None, - "python_version": "source", - "size": 200, - "upload_time": files[0].upload_time.strftime( - "%Y-%m-%dT%H:%M:%S" - ), - "upload_time_iso_8601": files[0].upload_time.isoformat() + "Z", - "url": "/the/fake/url/", - "requires_python": None, - "yanked": False, - "yanked_reason": None, - } - ], - "2.0": [ - { - "comment_text": None, - "downloads": -1, - "filename": files[1].filename, - "has_sig": True, - "md5_digest": files[1].md5_digest, - "digests": { - "md5": files[1].md5_digest, - "sha256": files[1].sha256_digest, - }, - "packagetype": None, - "python_version": "source", - "size": 200, - "upload_time": files[1].upload_time.strftime( - "%Y-%m-%dT%H:%M:%S" - ), - "upload_time_iso_8601": files[1].upload_time.isoformat() + "Z", - "url": "/the/fake/url/", - "requires_python": None, - "yanked": False, - "yanked_reason": None, - } - ], - "3.0": [ + assert db_request.response.body == _json.dumps( + { + "info": { + "author": None, + "author_email": None, + "bugtrack_url": None, + "classifiers": [], + "description_content_type": description_content_type, + "description": releases[-1].description.raw, + "docs_url": "/the/fake/url/", + "download_url": None, + "downloads": {"last_day": -1, "last_week": -1, "last_month": -1}, + "home_page": None, + "keywords": None, + "license": None, + "maintainer": None, + "maintainer_email": None, + "name": project.name, + "package_url": "/the/fake/url/", + "platform": None, + "project_url": "/the/fake/url/", + "project_urls": expected_urls, + "release_url": "/the/fake/url/", + "requires_dist": None, + "requires_python": None, + "summary": None, + "yanked": False, + "yanked_reason": None, + "version": "3.0", + }, + "releases": { + "0.1": [], + "1.0": [ + { + "comment_text": None, + "downloads": -1, + "filename": files[0].filename, + "has_sig": True, + "md5_digest": files[0].md5_digest, + "digests": { + "md5": files[0].md5_digest, + "sha256": files[0].sha256_digest, + }, + "packagetype": None, + "python_version": "source", + "size": 200, + "upload_time": files[0].upload_time.strftime( + "%Y-%m-%dT%H:%M:%S" + ), + "upload_time_iso_8601": files[0].upload_time.isoformat() + + "Z", + "url": "/the/fake/url/", + "requires_python": None, + "yanked": False, + "yanked_reason": None, + } + ], + "2.0": [ + { + "comment_text": None, + "downloads": -1, + "filename": files[1].filename, + "has_sig": True, + "md5_digest": files[1].md5_digest, + "digests": { + "md5": files[1].md5_digest, + "sha256": files[1].sha256_digest, + }, + "packagetype": None, + "python_version": "source", + "size": 200, + "upload_time": files[1].upload_time.strftime( + "%Y-%m-%dT%H:%M:%S" + ), + "upload_time_iso_8601": files[1].upload_time.isoformat() + + "Z", + "url": "/the/fake/url/", + "requires_python": None, + "yanked": False, + "yanked_reason": None, + } + ], + "3.0": [ + { + "comment_text": None, + "downloads": -1, + "filename": files[2].filename, + "has_sig": True, + "md5_digest": files[2].md5_digest, + "digests": { + "md5": files[2].md5_digest, + "sha256": files[2].sha256_digest, + }, + "packagetype": None, + "python_version": "source", + "size": 200, + "upload_time": files[2].upload_time.strftime( + "%Y-%m-%dT%H:%M:%S" + ), + "upload_time_iso_8601": files[2].upload_time.isoformat() + + "Z", + "url": "/the/fake/url/", + "requires_python": None, + "yanked": False, + "yanked_reason": None, + } + ], + }, + "urls": [ { "comment_text": None, "downloads": -1, @@ -418,32 +449,11 @@ def test_detail_renders(self, pyramid_config, db_request, db_session): "yanked_reason": None, } ], + "last_serial": je.id, + "vulnerabilities": [], }, - "urls": [ - { - "comment_text": None, - "downloads": -1, - "filename": files[2].filename, - "has_sig": True, - "md5_digest": files[2].md5_digest, - "digests": { - "md5": files[2].md5_digest, - "sha256": files[2].sha256_digest, - }, - "packagetype": None, - "python_version": "source", - "size": 200, - "upload_time": files[2].upload_time.strftime("%Y-%m-%dT%H:%M:%S"), - "upload_time_iso_8601": files[2].upload_time.isoformat() + "Z", - "url": "/the/fake/url/", - "requires_python": None, - "yanked": False, - "yanked_reason": None, - } - ], - "last_serial": je.id, - "vulnerabilities": [], - } + sort_keys=True, + ).encode("utf8") def test_minimal_renders(self, pyramid_config, db_request): project = ProjectFactory.create(has_docs=False) @@ -463,7 +473,7 @@ def test_minimal_renders(self, pyramid_config, db_request): url = "/the/fake/url/" db_request.route_url = pretend.call_recorder(lambda *args, **kw: url) - result = json.json_release(release, db_request) + json.json_release(release, db_request) assert set(db_request.route_url.calls) == { pretend.call("packaging.file", path=file.path), @@ -476,37 +486,63 @@ def test_minimal_renders(self, pyramid_config, db_request): _assert_has_cors_headers(db_request.response.headers) assert db_request.response.headers["X-PyPI-Last-Serial"] == str(je.id) - assert result == { - "info": { - "author": None, - "author_email": None, - "bugtrack_url": None, - "classifiers": [], - "description_content_type": release.description.content_type, - "description": release.description.raw, - "docs_url": None, - "download_url": None, - "downloads": {"last_day": -1, "last_week": -1, "last_month": -1}, - "home_page": None, - "keywords": None, - "license": None, - "maintainer": None, - "maintainer_email": None, - "name": project.name, - "package_url": "/the/fake/url/", - "platform": None, - "project_url": "/the/fake/url/", - "project_urls": None, - "release_url": "/the/fake/url/", - "requires_dist": None, - "requires_python": None, - "summary": None, - "yanked": False, - "yanked_reason": None, - "version": "0.1", - }, - "releases": { - "0.1": [ + assert db_request.response.body == _json.dumps( + { + "info": { + "author": None, + "author_email": None, + "bugtrack_url": None, + "classifiers": [], + "description_content_type": release.description.content_type, + "description": release.description.raw, + "docs_url": None, + "download_url": None, + "downloads": {"last_day": -1, "last_week": -1, "last_month": -1}, + "home_page": None, + "keywords": None, + "license": None, + "maintainer": None, + "maintainer_email": None, + "name": project.name, + "package_url": "/the/fake/url/", + "platform": None, + "project_url": "/the/fake/url/", + "project_urls": None, + "release_url": "/the/fake/url/", + "requires_dist": None, + "requires_python": None, + "summary": None, + "yanked": False, + "yanked_reason": None, + "version": "0.1", + }, + "releases": { + "0.1": [ + { + "comment_text": None, + "downloads": -1, + "filename": file.filename, + "has_sig": True, + "md5_digest": file.md5_digest, + "digests": { + "md5": file.md5_digest, + "sha256": file.sha256_digest, + }, + "packagetype": None, + "python_version": "source", + "size": 200, + "upload_time": file.upload_time.strftime( + "%Y-%m-%dT%H:%M:%S" + ), + "upload_time_iso_8601": file.upload_time.isoformat() + "Z", + "url": "/the/fake/url/", + "requires_python": None, + "yanked": False, + "yanked_reason": None, + } + ] + }, + "urls": [ { "comment_text": None, "downloads": -1, @@ -527,30 +563,12 @@ def test_minimal_renders(self, pyramid_config, db_request): "yanked": False, "yanked_reason": None, } - ] + ], + "last_serial": je.id, + "vulnerabilities": [], }, - "urls": [ - { - "comment_text": None, - "downloads": -1, - "filename": file.filename, - "has_sig": True, - "md5_digest": file.md5_digest, - "digests": {"md5": file.md5_digest, "sha256": file.sha256_digest}, - "packagetype": None, - "python_version": "source", - "size": 200, - "upload_time": file.upload_time.strftime("%Y-%m-%dT%H:%M:%S"), - "upload_time_iso_8601": file.upload_time.isoformat() + "Z", - "url": "/the/fake/url/", - "requires_python": None, - "yanked": False, - "yanked_reason": None, - } - ], - "last_serial": je.id, - "vulnerabilities": [], - } + sort_keys=True, + ).encode("utf8") def test_vulnerabilities_renders(self, pyramid_config, db_request): project = ProjectFactory.create(has_docs=False) @@ -570,7 +588,7 @@ def test_vulnerabilities_renders(self, pyramid_config, db_request): result = json.json_release(release, db_request) - assert result["vulnerabilities"] == [ + assert _json.loads(result.body)["vulnerabilities"] == [ { "id": "PYSEC-001", "source": "the source", diff --git a/warehouse/legacy/api/json.py b/warehouse/legacy/api/json.py index 0f37d5e310a8..bd911ae64806 100644 --- a/warehouse/legacy/api/json.py +++ b/warehouse/legacy/api/json.py @@ -10,6 +10,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import json + from pyramid.httpexceptions import HTTPMovedPermanently, HTTPNotFound from pyramid.view import view_config from sqlalchemy.orm import Load @@ -84,7 +86,6 @@ def json_project(project, request): @view_config( route_name="legacy.api.json.project_slash", context=Project, - renderer="json", decorator=_CACHE_DECORATOR, ) def json_project_slash(project, request): @@ -94,7 +95,6 @@ def json_project_slash(project, request): @view_config( route_name="legacy.api.json.release", context=Release, - renderer="json", decorator=_CACHE_DECORATOR, ) def json_release(release, request): @@ -177,7 +177,7 @@ def json_release(release, request): for vulnerability_record in release.vulnerabilities ] - return { + data = { "info": { "name": project.name, "version": release.version, @@ -216,6 +216,18 @@ def json_release(release, request): "last_serial": project.last_serial, } + # Stream the results to the client instead of building them up, this will + # make it so that the JSON encoder uses less memory overall. + resp = request.response + resp.content_type = "application/json" + resp.app_iter = ( + c.encode("utf8") + for c in json.JSONEncoder(sort_keys=True, separators=(", ", ": ")).iterencode( + data + ) + ) + return resp + @view_config( route_name="legacy.api.json.release_slash",