Repository: ansible/ansible-lint

Issue:
Packaging problems: broken PyPI metadata and related suggestions
This is similar to what I've proposed @ willthames/ansible-review#70.
cc @gundalow

# Issue Type
- Bug report
- Feature request
- (I think it kinda combines both)

#  Ansible Lint details

https://pypi.org/project/ansible-lint/#description:
```
Project description
The author of this package has not provided a project description
```

# Desired Behaviour

- [x] 1. README contents present on PyPI.
- [x] 2. Project version not hardcoded in Python, but is sourced from a Git tag
- [x] 3. `project_urls` metadata on PyPI
- [x] 4. Metadata in declarative `setup.cfg` (with `setup.py` only containing a shim for old `setuptools`)
- [x] 5. Both wheel and sdist published
- [x] 6. Building/publishing automated

# Actual Behaviour (Bug report only)

None of above.


Discussion:

Resolving patch:
diff --git a/lib/ansiblelint/version.py b/lib/ansiblelint/version.py
--- a/lib/ansiblelint/version.py
+++ b/lib/ansiblelint/version.py
@@ -1 +1,15 @@
-__version__ = '3.5.1'
+"""Ansible-lint version information."""
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+try:
+    import pkg_resources
+except ImportError:
+    pass
+
+
+try:
+    __version__ = pkg_resources.get_distribution('ansible-lint').version
+except Exception:
+    __version__ = 'unknown'
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -1,30 +1,135 @@
-import os
-from setuptools import find_packages
-from setuptools import setup
-import sys
-
-
-sys.path.insert(0, os.path.abspath('lib'))
-
-exec(open('lib/ansiblelint/version.py').read())
-
-setup(
-    name='ansible-lint',
-    version=__version__,
-    description='checks playbooks for practices and behaviour that could potentially be improved',
-    keywords='ansible, lint',
-    maintainer='Ansible by Red Hat',
-    maintainer_email='info@ansible.com',
-    url='https://github.com/ansible/ansible-lint',
-    package_dir={'': 'lib'},
-    packages=find_packages('lib'),
-    zip_safe=False,
-    install_requires=['ansible', 'pyyaml', 'six'],
-    entry_points={
-        'console_scripts': [
-            'ansible-lint = ansiblelint.__main__:main'
-        ]
-    },
-    license='MIT',
-    test_suite="test"
-)
+#! /usr/bin/env python
+"""Ansible-lint distribution package setuptools installer."""
+
+import setuptools
+
+
+try:
+    from setuptools.config import read_configuration, ConfigOptionsHandler
+    import setuptools.config
+    import setuptools.dist
+
+    # Set default value for 'use_scm_version'
+    setattr(setuptools.dist.Distribution, 'use_scm_version', False)
+
+    # Attach bool parser to 'use_scm_version' option
+    class ShimConfigOptionsHandler(ConfigOptionsHandler):
+        """Extension class for ConfigOptionsHandler."""
+
+        @property
+        def parsers(self):
+            """Return an option mapping with default data type parsers."""
+            _orig_parsers = super(ShimConfigOptionsHandler, self).parsers
+            return dict(use_scm_version=self._parse_bool, **_orig_parsers)
+
+    setuptools.config.ConfigOptionsHandler = ShimConfigOptionsHandler
+except ImportError:
+    """This is a shim for setuptools<30.3."""
+    import io
+    import json
+
+    try:
+        from configparser import ConfigParser, NoSectionError
+    except ImportError:
+        from ConfigParser import ConfigParser, NoSectionError
+        ConfigParser.read_file = ConfigParser.readfp
+
+    def maybe_read_files(d):
+        """Read files if the string starts with `file:` marker."""
+        d = d.strip()
+        if not d.startswith('file:'):
+            return d
+        descs = []
+        for fname in map(str.strip, d[5:].split(',')):
+            with io.open(fname, encoding='utf-8') as f:
+                descs.append(f.read())
+        return ''.join(descs)
+
+    def cfg_val_to_list(v):
+        """Turn config val to list and filter out empty lines."""
+        return list(filter(bool, map(str.strip, v.strip().splitlines())))
+
+    def cfg_val_to_dict(v):
+        """Turn config val to dict and filter out empty lines."""
+        return dict(
+            map(lambda l: list(map(str.strip, l.split('=', 1))),
+                filter(bool, map(str.strip, v.strip().splitlines())))
+        )
+
+    def cfg_val_to_primitive(v):
+        """Parse primitive config val to appropriate data type."""
+        return json.loads(v.strip().lower())
+
+    def read_configuration(filepath):
+        """Read metadata and options from setup.cfg located at filepath."""
+        cfg = ConfigParser()
+        with io.open(filepath, encoding='utf-8') as f:
+            cfg.read_file(f)
+
+        md = dict(cfg.items('metadata'))
+        for list_key in 'classifiers', 'keywords':
+            try:
+                md[list_key] = cfg_val_to_list(md[list_key])
+            except KeyError:
+                pass
+        try:
+            md['long_description'] = maybe_read_files(md['long_description'])
+        except KeyError:
+            pass
+        opt = dict(cfg.items('options'))
+        for list_key in 'use_scm_version', 'zip_safe':
+            try:
+                opt[list_key] = cfg_val_to_primitive(opt[list_key])
+            except KeyError:
+                pass
+        for list_key in 'scripts', 'install_requires', 'setup_requires':
+            try:
+                opt[list_key] = cfg_val_to_list(opt[list_key])
+            except KeyError:
+                pass
+        try:
+            opt['package_dir'] = cfg_val_to_dict(opt['package_dir'])
+        except KeyError:
+            pass
+        opt_package_data = dict(cfg.items('options.package_data'))
+        try:
+            if not opt_package_data.get('', '').strip():
+                opt_package_data[''] = opt_package_data['*']
+                del opt_package_data['*']
+        except KeyError:
+            pass
+        try:
+            opt_extras_require = dict(cfg.items('options.extras_require'))
+            opt['extras_require'] = {}
+            for k, v in opt_extras_require.items():
+                opt['extras_require'][k] = cfg_val_to_list(v)
+        except NoSectionError:
+            pass
+        opt['package_data'] = {}
+        for k, v in opt_package_data.items():
+            opt['package_data'][k] = cfg_val_to_list(v)
+        cur_pkgs = opt.get('packages', '').strip()
+        if '\n' in cur_pkgs:
+            opt['packages'] = cfg_val_to_list(opt['packages'])
+        elif cur_pkgs.startswith('find:'):
+            opt_packages_find = dict(cfg.items('options.packages.find'))
+            opt['packages'] = setuptools.find_packages(**opt_packages_find)
+        return {'metadata': md, 'options': opt}
+
+
+setup_params = {}
+declarative_setup_params = read_configuration('setup.cfg')
+
+# Patch incorrectly decoded package_dir option
+# ``egg_info`` demands native strings failing with unicode under Python 2
+# Ref https://github.com/pypa/setuptools/issues/1136
+declarative_setup_params['options']['package_dir'] = {
+    str(k): str(v)
+    for k, v in declarative_setup_params['options']['package_dir'].items()
+}
+
+setup_params = dict(setup_params, **declarative_setup_params['metadata'])
+setup_params = dict(setup_params, **declarative_setup_params['options'])
+
+
+__name__ == '__main__' and setuptools.setup(**setup_params)


Test patch: