Repository: pypi/warehouse

Issue:
Improved error message when attempting upload to Warehouse via twine with an invalid trove classifier
I just now uploaded a new release to PyPI. Initially, I had made a typo in one of my trove classifiers (`Topic :: Testing` instead of `Topic :: Software Development :: Testing`).

The error returned by `twine` when I attempted to upload to Warehouse was mostly unhelpful, other than indicating something was wrong with my classifiers:

```
HTTPError: 400 Client Error: ['License :: OSI Approved :: MIT License', 'Natural Language :: English', 'Intended Audience :: Developers', 'Operating System :: OS Independent', 'Programming Language :: Python :: 3 :: Only', 'Programming Language :: Python :: 3.4', 'Programming Languag for url: https://upload.pypi.org/legacy/
```

The error returned by `twine` when I attempted an upload to `testpypi`, though, pointed me (eventually) to the problematic classifier:

```
HTTPError: 400 Client Error: ['License :: OSI Approved :: MIT License', 'Natural Language :: English', 'Intended Audience :: Developers', 'Operating System :: OS Independent', 'Programming Language :: Python :: 3 :: Only', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: Testing', 'Development Status :: 4 - Beta'] is an invalid value for Classifier. Error: 'Topic :: Testing' is not a valid choice for this field see https://packaging.python.org/specifications/core-metadata for url: https://test.pypi.org/legacy/
```

A more succinct, targeted error message here would be hugely helpful.

FWIW, it almost looks like Warehouse tried to generate the same message as legacy, but it got clipped?


Discussion:
https://github.com/pypa/warehouse/blob/e7bacffd8bb505b3811750f3b4e366ab1b3d157d/warehouse/forklift/legacy.py#L415-L417 is the field we'd need to update. I _think_ we could improve the error message with a custom validator?
Thanks, @bskinn. I just want to make sure -- are you saying this is a regression and worse behavior, compared to legacy PyPI?
@brainwane Yes, it appears to be a regression -- with legacy PyPI, the full error message was echoed to console, and so I was at least able to immediately see the offending classifier, even if I had to stare at it for a bit to find the relevant part, at the end.

Resolving patch:
diff --git a/warehouse/forklift/legacy.py b/warehouse/forklift/legacy.py
--- a/warehouse/forklift/legacy.py
+++ b/warehouse/forklift/legacy.py
@@ -760,19 +760,22 @@ def file_upload(request):
             field_name = sorted(form.errors.keys())[0]
 
         if field_name in form:
-            if form[field_name].description:
+            field = form[field_name]
+            if field.description and isinstance(field, wtforms.StringField):
                 error_message = (
                     "{value!r} is an invalid value for {field}. ".format(
-                        value=form[field_name].data,
-                        field=form[field_name].description) +
+                        value=field.data,
+                        field=field.description) +
                     "Error: {} ".format(form.errors[field_name][0]) +
                     "see "
                     "https://packaging.python.org/specifications/core-metadata"
                 )
             else:
-                error_message = "{field}: {msgs[0]}".format(
-                    field=field_name,
-                    msgs=form.errors[field_name],
+                error_message = (
+                    "Invalid value for {field}. Error: {msgs[0]}".format(
+                        field=field_name,
+                        msgs=form.errors[field_name],
+                    )
                 )
         else:
             error_message = "Error: {}".format(form.errors[field_name][0])


Test patch:
diff --git a/tests/unit/forklift/test_legacy.py b/tests/unit/forklift/test_legacy.py
--- a/tests/unit/forklift/test_legacy.py
+++ b/tests/unit/forklift/test_legacy.py
@@ -814,7 +814,7 @@ def test_fails_invalid_version(self, pyramid_config, pyramid_request,
                     "version": "1.0",
                     "md5_digest": "bad",
                 },
-                "filetype: This field is required.",
+                "Invalid value for filetype. Error: This field is required.",
             ),
             (
                 {
@@ -835,7 +835,7 @@ def test_fails_invalid_version(self, pyramid_config, pyramid_request,
                     "pyversion": "1.0",
                     "md5_digest": "bad",
                 },
-                "filetype: Unknown type of file.",
+                "Invalid value for filetype. Error: Unknown type of file.",
             ),
             (
                 {
@@ -867,8 +867,8 @@ def test_fails_invalid_version(self, pyramid_config, pyramid_request,
                     "filetype": "sdist",
                     "sha256_digest": "an invalid sha256 digest",
                 },
-                "sha256_digest: "
-                "Must be a valid, hex encoded, SHA256 message digest."
+                "Invalid value for sha256_digest. "
+                "Error: Must be a valid, hex encoded, SHA256 message digest."
             ),
 
             # summary errors
@@ -1494,11 +1494,9 @@ def test_upload_fails_with_invalid_classifier(self, pyramid_config,
 
         assert resp.status_code == 400
         assert resp.status == (
-            "400 ['Environment :: Other Environment'] "
-            "is an invalid value for Classifier. "
+            "400 Invalid value for classifiers. "
             "Error: 'Environment :: Other Environment' is not a valid choice "
-            "for this field "
-            "see https://packaging.python.org/specifications/core-metadata"
+            "for this field"
         )
 
     @pytest.mark.parametrize(