Repository: pypa/setuptools

Issue:
maintainer= generates AUTHOR: line in PKG-INFO
This seems like a deliberate choice, but it seems that at the moment the `Author:` metadata is pulled preferentially from `maintainer=`, and `Maintainer:` is never actually generated.

The implementation is [here](https://github.com/pypa/setuptools/blob/d10a1bb476db25dc888b5b4aad9d81ffa245ea30/setuptools/dist.py#L55), and the implementation of `get_contact` that prioritizes `maintainer` is [here](https://github.com/python/cpython/blob/8837dd092fe5ad5184889104e8036811ed839f98/Lib/distutils/dist.py#L1119).

I suspect that this is an artifact of the fact that [PEP 314](https://www.python.org/dev/peps/pep-0314/#author-optional) does not provide a `Maintainer` field, and that was a later addition in [PEP 345](https://www.python.org/dev/peps/pep-0345/#maintainer-email-optional)?

I can make a PR for this, but there is an implementation question. The options I see right now are:

1. Maintain the current behavior and simply *add* a "Maintainer" field. This is the most "backwards compatible" way to do things.
2. Change the behavior so that `author=` is given preference over `maintainer=` when generating the `AUTHOR` field, but still use `maintainer` if no `author` is specified. Additionally always add the `Maintainer` field if `maintainer=` is present.
3. Change the behavior so that *only* `author=` generates the `Author` field and *only* `maintainer=` generates the `Maintainer:` field. This is the least backwards-incompatible change.

I suspect that the choice of these probably should come down to how PyPI and Warehouse handle the metadata. If both PyPI and Warehouse are able to handle an either/or Author/Maintainer field, then I suggest that the best option is 3. Basically the fallback logic should be on the metadata consumer side, not on the metadata *producer* side.


Discussion:
I've made a new issue for [distutils](https://bugs.python.org/issue33069), with a PR at python/cpython#6106

Resolving patch:
diff --git a/setuptools/dist.py b/setuptools/dist.py
--- a/setuptools/dist.py
+++ b/setuptools/dist.py
@@ -14,6 +14,7 @@
     DistutilsOptionError, DistutilsPlatformError, DistutilsSetupError,
 )
 from distutils.util import rfc822_escape
+from distutils.version import StrictVersion
 
 from setuptools.extern import six
 from setuptools.extern.six.moves import map, filter, filterfalse
@@ -34,28 +35,46 @@ def _get_unpatched(cls):
     warnings.warn("Do not call this function", DeprecationWarning)
     return get_unpatched(cls)
 
+def get_metadata_version(dist_md):
+    if dist_md.long_description_content_type or dist_md.provides_extras:
+        return StrictVersion('2.1')
+    elif getattr(dist_md, 'python_requires', None) is not None:
+        return StrictVersion('1.2')
+    elif (dist_md.provides or dist_md.requires or dist_md.obsoletes or
+            dist_md.classifiers or dist_md.download_url):
+        return StrictVersion('1.1')
+
+    return StrictVersion('1.0')
+
 
 # Based on Python 3.5 version
 def write_pkg_file(self, file):
     """Write the PKG-INFO format data to a file object.
     """
-    version = '1.0'
-    if (self.provides or self.requires or self.obsoletes or
-            self.classifiers or self.download_url):
-        version = '1.1'
-    # Setuptools specific for PEP 345
-    if hasattr(self, 'python_requires') or self.project_urls:
-        version = '1.2'
-    if self.long_description_content_type or self.provides_extras:
-        version = '2.1'
+    version = get_metadata_version(self)
 
     file.write('Metadata-Version: %s\n' % version)
     file.write('Name: %s\n' % self.get_name())
     file.write('Version: %s\n' % self.get_version())
     file.write('Summary: %s\n' % self.get_description())
     file.write('Home-page: %s\n' % self.get_url())
-    file.write('Author: %s\n' % self.get_contact())
-    file.write('Author-email: %s\n' % self.get_contact_email())
+
+    if version == '1.2':
+        file.write('Author: %s\n' % self.get_contact())
+        file.write('Author-email: %s\n' % self.get_contact_email())
+    else:
+        optional_fields = (
+            ('Author', 'author'),
+            ('Author-email', 'author_email'),
+            ('Maintainer', 'maintainer'),
+            ('Maintainer-email', 'maintainer_email'),
+        )
+
+        for field, attr in optional_fields:
+            attr_val = getattr(self, attr)
+            if attr_val is not None:
+                file.write('%s: %s\n' % (field, attr_val))
+
     file.write('License: %s\n' % self.get_license())
     if self.download_url:
         file.write('Download-URL: %s\n' % self.download_url)
@@ -69,7 +88,12 @@ def write_pkg_file(self, file):
     if keywords:
         file.write('Keywords: %s\n' % keywords)
 
-    self._write_list(file, 'Platform', self.get_platforms())
+    if version == '1.2':
+        for platform in self.get_platforms():
+            file.write('Platform: %s\n' % platform)
+    else:
+        self._write_list(file, 'Platform', self.get_platforms())
+
     self._write_list(file, 'Classifier', self.get_classifiers())
 
     # PEP 314


Test patch:
diff --git a/setuptools/tests/test_dist.py b/setuptools/tests/test_dist.py
--- a/setuptools/tests/test_dist.py
+++ b/setuptools/tests/test_dist.py
@@ -1,10 +1,13 @@
+# -*- coding: utf-8 -*-
 from setuptools import Distribution
 from setuptools.extern.six.moves.urllib.request import pathname2url
 from setuptools.extern.six.moves.urllib_parse import urljoin
+from setuptools.extern.six import StringIO
 
 from .textwrap import DALS
 from .test_easy_install import make_nspkg_sdist
 
+import pytest
 
 def test_dist_fetch_build_egg(tmpdir):
     """
@@ -45,3 +48,78 @@ def sdist_with_index(distname, version):
             for r in reqs
         ]
     assert [dist.key for dist in resolved_dists if dist] == reqs
+
+
+def __maintainer_test_cases():
+    attrs = {"name": "package",
+             "version": "1.0",
+             "description": "xxx"}
+
+    def merge_dicts(d1, d2):
+        d1 = d1.copy()
+        d1.update(d2)
+
+        return d1
+
+    test_cases = [
+        ('No author, no maintainer', attrs.copy()),
+        ('Author (no e-mail), no maintainer', merge_dicts(attrs,
+            {'author': 'Author Name'})),
+        ('Author (e-mail), no maintainer', merge_dicts(attrs,
+            {'author': 'Author Name',
+             'author_email': 'author@name.com'})),
+        ('No author, maintainer (no e-mail)', merge_dicts(attrs,
+            {'maintainer': 'Maintainer Name'})),
+        ('No author, maintainer (e-mail)', merge_dicts(attrs,
+            {'maintainer': 'Maintainer Name',
+             'maintainer_email': 'maintainer@name.com'})),
+        ('Author (no e-mail), Maintainer (no-email)', merge_dicts(attrs,
+            {'author': 'Author Name',
+             'maintainer': 'Maintainer Name'})),
+        ('Author (e-mail), Maintainer (e-mail)', merge_dicts(attrs,
+            {'author': 'Author Name',
+             'author_email': 'author@name.com',
+             'maintainer': 'Maintainer Name',
+             'maintainer_email': 'maintainer@name.com'})),
+        ('No author (e-mail), no maintainer (e-mail)', merge_dicts(attrs,
+            {'author_email': 'author@name.com',
+             'maintainer_email': 'maintainer@name.com'})),
+        ('Author unicode', merge_dicts(attrs,
+            {'author': '鉄沢寛'})),
+        ('Maintainer unicode', merge_dicts(attrs,
+            {'maintainer': 'Jan Łukasiewicz'})),
+    ]
+
+    return test_cases
+
+@pytest.mark.parametrize('name,attrs', __maintainer_test_cases())
+def test_maintainer_author(name, attrs):
+    tested_keys = {
+        'author': 'Author',
+        'author_email': 'Author-email',
+        'maintainer': 'Maintainer',
+        'maintainer_email': 'Maintainer-email',
+    }
+
+    # Generate a PKG-INFO file
+    dist = Distribution(attrs)
+    PKG_INFO = StringIO()
+    dist.metadata.write_pkg_file(PKG_INFO)
+    PKG_INFO.seek(0)
+
+    pkg_lines = PKG_INFO.readlines()
+    pkg_lines = [_ for _ in pkg_lines if _]   # Drop blank lines
+    pkg_lines_set = set(pkg_lines)
+
+    # Duplicate lines should not be generated
+    assert len(pkg_lines) == len(pkg_lines_set)
+
+    for fkey, dkey in tested_keys.items():
+        val = attrs.get(dkey, None)
+        if val is None:
+            for line in pkg_lines:
+                assert not line.startswith(fkey + ':')
+        else:
+            line = '%s: %s' % (fkey, val)
+            assert line in pkg_lines_set
+