Imported Upstream version 39.2.0 upstream/39.2.0
authorDongHun Kwak <dh0128.kwak@samsung.com>
Mon, 14 Jan 2019 01:42:29 +0000 (10:42 +0900)
committerDongHun Kwak <dh0128.kwak@samsung.com>
Mon, 14 Jan 2019 01:42:29 +0000 (10:42 +0900)
29 files changed:
.github/pull_request_template.md [new file with mode: 0644]
.gitignore
.travis.yml
CHANGES.rst
README.rst
appveyor.yml
changelog.d/1364.rst [new file with mode: 0644]
docs/conf.py
docs/developer-guide.txt
docs/pkg_resources.txt
docs/releases.txt
docs/setuptools.txt
netlify.toml [new file with mode: 0644]
pkg_resources/__init__.py
pkg_resources/tests/test_resources.py
pyproject.toml [new file with mode: 0644]
setup.cfg
setup.py
setuptools/config.py
setuptools/dist.py
setuptools/tests/test_config.py
setuptools/tests/test_egg_info.py
setuptools/tests/test_glibc.py [new file with mode: 0644]
setuptools/tests/test_pep425tags.py [new file with mode: 0644]
setuptools/tests/test_wheel.py
setuptools/wheel.py
tests/requirements.txt
towncrier_template.rst [new file with mode: 0644]
tox.ini

diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644 (file)
index 0000000..aa55080
--- /dev/null
@@ -0,0 +1,12 @@
+<!-- First time contributors: Take a moment to review https://setuptools.readthedocs.io/en/latest/developer-guide.html! -->
+<!-- Remove sections if not applicable -->
+
+## Summary of changes
+
+<!-- Summary goes here -->
+
+Closes <!-- issue number here -->
+
+### Pull Request Checklist
+- [ ] Changes have tests
+- [ ] News fragment added in changelog.d. See [documentation](http://setuptools.readthedocs.io/en/latest/developer-guide.html#making-a-pull-request) for details
index f5661164f75421fb2d43a4db35af25a954e67c33..b850622f3f76e6e4f6b4febe5ee73fef3a3fc1b0 100644 (file)
@@ -2,6 +2,7 @@
 bin
 build
 dist
+docs/build
 include
 lib
 distribute.egg-info
index ced8fa6bcc7d2c4979bedde6ed70b5425d0ff03f..63d0333ae6fe6155a5093fbbca8b82495b498698 100644 (file)
@@ -3,7 +3,6 @@ sudo: false
 language: python
 python:
 - &latest_py2 2.7
-- 3.3
 - 3.4
 - 3.5
 - &latest_py3 3.6
@@ -40,8 +39,10 @@ jobs:
 cache: pip
 
 install:
+# ensure we have recent pip/setuptools
+- pip install --upgrade pip setuptools
 # need tox to get started
-- pip install tox 'tox-venv; python_version!="3.3"'
+- pip install tox tox-venv
 
 # Output the env, to verify behavior
 - env
index d8b5b49147ad67db4a2eaf8901d4e2065ac68478..d3d661cc3ebebc95fc33e963a2c34216e952e292 100644 (file)
@@ -1,3 +1,29 @@
+v39.2.0
+-------
+
+* #1359: Support using "file:" to load a PEP 440-compliant package version from
+  a text file.
+* #1360: Fixed issue with a mismatch between the name of the package and the
+  name of the .dist-info file in wheel files
+* #1365: Take the package_dir option into account when loading the version from
+  a module attribute.
+* #1353: Added coverage badge to README.
+* #1356: Made small fixes to the developer guide documentation.
+* #1357: Fixed warnings in documentation builds and started enforcing that the
+  docs build without warnings in tox.
+* #1376: Updated release process docs.
+* #1343: The ``setuptools`` specific ``long_description_content_type``,
+  ``project_urls`` and ``provides_extras`` fields are now set consistently
+  after any ``distutils`` ``setup_keywords`` calls, allowing them to override
+  values.
+* #1352: Added ``tox`` environment for documentation builds.
+* #1354: Added ``towncrier`` for changelog managment.
+* #1355: Add PR template.
+* #1368: Fixed tests which failed without network connectivity.
+* #1369: Added unit tests for PEP 425 compatibility tags support.
+* #1372: Stop testing Python 3.3 in Travis CI, now that the latest version of
+  ``wheel`` no longer installs on it.
+
 v39.1.0
 -------
 
index f754d96624bc4ff32b54c94c0c73d1308eef51fb..2c008cc6e386d70ac7d5f7645fbb08439e8100d8 100755 (executable)
@@ -7,8 +7,11 @@
 .. image:: https://img.shields.io/travis/pypa/setuptools/master.svg?label=Linux%20build%20%40%20Travis%20CI
    :target: https://travis-ci.org/pypa/setuptools
 
-.. image:: https://img.shields.io/appveyor/ci/jaraco/setuptools/master.svg?label=Windows%20build%20%40%20Appveyor
-   :target: https://ci.appveyor.com/project/jaraco/setuptools/branch/master
+.. image:: https://img.shields.io/appveyor/ci/pypa/setuptools/master.svg?label=Windows%20build%20%40%20Appveyor
+   :target: https://ci.appveyor.com/project/pypa/setuptools/branch/master
+
+.. image:: https://img.shields.io/codecov/c/github/pypa/setuptools/master.svg
+   :target: https://codecov.io/gh/pypa/setuptools
 
 .. image:: https://img.shields.io/pypi/pyversions/setuptools.svg
 
index ff7122b41fb17d4938b402c1e27617cea95f444a..f50f8386ac7f8331ada90fc88f296978dfcac5e9 100644 (file)
@@ -4,8 +4,8 @@ environment:
   CODECOV_ENV: APPVEYOR_JOB_NAME
 
   matrix:
-    - APPVEYOR_JOB_NAME: "python35-x64"
-      PYTHON: "C:\\Python35-x64"
+    - APPVEYOR_JOB_NAME: "python36-x64"
+      PYTHON: "C:\\Python36-x64"
     - APPVEYOR_JOB_NAME: "python27-x64"
       PYTHON: "C:\\Python27-x64"
 
diff --git a/changelog.d/1364.rst b/changelog.d/1364.rst
new file mode 100644 (file)
index 0000000..f7b4c01
--- /dev/null
@@ -0,0 +1 @@
+Add `__dir__()` implementation to `pkg_resources.Distribution()` that includes the attributes in the `_provider` instance variable.
\ No newline at end of file
index f7d02303cce915463c23e6f342dabc589e9514ef..c7eb6d3f3c48282b9af4e69741908cce4ee6bf80 100644 (file)
@@ -34,7 +34,7 @@ import os
 
 # Add any Sphinx extension module names here, as strings. They can be extensions
 # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
-extensions = ['jaraco.packaging.sphinx', 'rst.linker', 'sphinx.ext.autosectionlabel']
+extensions = ['jaraco.packaging.sphinx', 'rst.linker']
 
 # Add any paths that contain templates here, relative to this directory.
 templates_path = ['_templates']
@@ -45,6 +45,9 @@ source_suffix = '.txt'
 # The master toctree document.
 master_doc = 'index'
 
+# A list of glob-style patterns that should be excluded when looking for source files.
+exclude_patterns = ['requirements.txt']
+
 # List of directories, relative to source directory, that shouldn't be searched
 # for source files.
 exclude_trees = []
index b2c1a0cefde364483c2f56f78bf5dce317bd08e1..6b04603b16c3c807d844f94ef83fb93584cd5609 100644 (file)
@@ -57,13 +57,45 @@ Setuptools makes extensive use of hyperlinks to tickets in the changelog so
 that system integrators and other users can get a quick summary, but then
 jump to the in-depth discussion about any subject referenced.
 
+---------------------
+Making a pull request
+---------------------
+
+When making a pull request, please include a short summary of the changes
+and a reference to any issue tickets that the PR is intended to solve.
+All PRs with code changes should include tests. All changes should include a
+changelog entry.
+
+``setuptools`` uses `towncrier <https://town-crier.readthedocs.io/en/latest/>`_
+for changelog managment, so when making a PR, please add a news fragment in the
+``changelog.d/`` folder. Changelog files are written in Restructured Text and
+should be a 1 or 2 sentence description of the substantive changes in the PR.
+They should be named ``<pr_number>.<category>.rst``, where the categories are:
+
+- ``change``: Any backwards compatible code change
+- ``breaking``: Any backwards-compatibility breaking change
+- ``doc``: A change to the documentation
+- ``misc``: Changes internal to the repo like CI, test and build changes
+- ``deprecation``: For deprecations of an existing feature of behavior
+
+A pull request may have more than one of these components, for example a code
+change may introduce a new feature that deprecates an old feature, in which
+case two fragments should be added. It is not necessary to make a separate
+documentation fragment for documentation changes accompanying the relevant
+code changes. See the following for an example news fragment:
+
+.. code-block:: bash
+
+    $ cat changelog.d/1288.change.rst
+    Add support for maintainer in PKG-INFO
+
 -----------
 Source Code
 -----------
 
 Grab the code at Github::
 
-    $ git checkout https://github.com/pypa/setuptools
+    $ git clone https://github.com/pypa/setuptools
 
 If you want to contribute changes, we recommend you fork the repository on
 Github, commit the changes to your repository, and then make a pull request
@@ -86,8 +118,12 @@ from this repository.
 Testing
 -------
 
-The primary tests are run using tox. To run the tests, first make
-sure you have tox installed, then invoke it::
+The primary tests are run using tox. To run the tests, first create the metadata
+needed to run the tests::
+
+    $ python bootstrap.py
+
+Then make sure you have tox installed, and invoke it::
 
     $ tox
 
@@ -106,10 +142,12 @@ Setuptools follows ``semver``.
 Building Documentation
 ----------------------
 
-Setuptools relies on the Sphinx system for building documentation.
-To accommodate RTD, docs must be built from the docs/ directory.
+Setuptools relies on the `Sphinx`_ system for building documentation.
+The `published documentation`_ is hosted on Read the Docs.
+
+To build the docs locally, use tox::
 
-To build them, you need to have installed the requirements specified
-in docs/requirements.txt. One way to do this is to use rwt:
+    $ tox -e docs
 
-    setuptools/docs$ python -m rwt -r requirements.txt -- -m sphinx . html
+.. _Sphinx: http://www.sphinx-doc.org/en/master/
+.. _published documentation: https://setuptools.readthedocs.io/en/latest/
index b40a209fe4be379f23741ea61f06cf61acd378ea..18545f4ba076d7fc9ff36b963e51f33c1ae7fa16 100644 (file)
@@ -1087,6 +1087,7 @@ so that supporting custom importers or new distribution formats can be done
 simply by creating an appropriate `IResourceProvider`_ implementation; see the
 section below on `Supporting Custom Importers`_ for more details.
 
+.. _ResourceManager API:
 
 ``ResourceManager`` API
 =======================
index 30ea084fec725098c934e44373e9c06ac91b2318..234f69eeb9ec8f217a048e912786bdffd627aae6 100644 (file)
@@ -7,20 +7,31 @@ mechanical technique for releases, enacted by Travis following a
 successful build of a tagged release per
 `PyPI deployment <https://docs.travis-ci.com/user/deployment/pypi>`_.
 
-Prior to cutting a release, please check that the CHANGES.rst reflects
-the summary of changes since the last release.
-Ideally, these changelog entries would have been added
-along with the changes, but it's always good to check.
-Think about it from the
-perspective of a user not involved with the development--what would
-that person want to know about what has changed--or from the
-perspective of your future self wanting to know when a particular
-change landed.
-
-To cut a release, install and run ``bump2version {part}`` where ``part``
+Prior to cutting a release, please use `towncrier`_ to update
+``CHANGES.rst`` to summarize the changes since the last release.
+To update the changelog:
+
+1. Install towncrier via ``pip install towncrier`` if not already installed.
+2. Preview the new ``CHANGES.rst`` entry by running
+   ``towncrier --draft --version {new.version.number}`` (enter the desired
+   version number for the next release).  If any changes are needed, make
+   them and generate a new preview until the output is acceptable.  Run
+   ``git add`` for any modified files.
+3. Run ``towncrier --version {new.version.number}`` to stage the changelog
+   updates in git.
+
+Once the changelog edits are staged and ready to commit, cut a release by
+installing and running ``bump2version {part}`` where ``part``
 is major, minor, or patch based on the scope of the changes in the
-release. Then, push the commits to the master branch. If tests pass,
-the release will be uploaded to PyPI (from the Python 3.6 tests).
+release. Then, push the commits to the master branch::
+
+    $ git push origin master
+    $ git push --tags
+
+If tests pass, the release will be uploaded to PyPI (from the Python 3.6
+tests).
+
+.. _towncrier: https://pypi.org/project/towncrier/
 
 Release Frequency
 -----------------
index e14d208881ddb8c3eb053d50a605d3cb60e2166a..f7b9351b78db9dd6612dcc60bca4a0a6b007db92 100644 (file)
@@ -242,7 +242,6 @@ have setuptools automatically tag your in-development releases with various
 pre- or post-release tags.  See the following sections for more details:
 
 * `Tagging and "Daily Build" or "Snapshot" Releases`_
-* `Managing "Continuous Releases" Using Subversion`_
 * The `egg_info`_ command
 
 
@@ -438,7 +437,7 @@ such projects also need something like ``package_dir={'':'src'}`` in their
 
 Anyway, ``find_packages()`` walks the target directory, filtering by inclusion
 patterns, and finds Python packages (any directory). Packages are only
-recognized if they include an ``__init__.py`` file. Finally, exclusion 
+recognized if they include an ``__init__.py`` file. Finally, exclusion
 patterns are applied to remove matching packages.
 
 Inclusion and exclusion patterns are package names, optionally including
@@ -1366,6 +1365,7 @@ then make an explicit declaration of ``True`` or ``False`` for the ``zip_safe``
 flag, so that it will not be necessary for ``bdist_egg`` or ``EasyInstall`` to
 try to guess whether your project can work as a zipfile.
 
+.. _Namespace Packages:
 
 Namespace Packages
 ------------------
@@ -2305,7 +2305,7 @@ Configuring setup() using setup.cfg files
 
 ``Setuptools`` allows using configuration files (usually :file:`setup.cfg`)
 to define a package’s metadata and other options that are normally supplied
-to the ``setup()`` function.
+to the ``setup()`` function (declarative config).
 
 This approach not only allows automation scenarios but also reduces
 boilerplate code in some cases.
@@ -2341,6 +2341,9 @@ boilerplate code in some cases.
     scripts =
       bin/first.py
       bin/second.py
+    install_requires =
+      requests
+      importlib; python_version == "2.6"
 
     [options.package_data]
     * = *.txt, *.rst
@@ -2421,7 +2424,7 @@ Metadata
 Key                             Aliases            Type
 ==============================  =================  =====
 name                                               str
-version                                            attr:, str
+version                                            attr:, file:, str
 url                             home-page          str
 download_url                    download-url       str
 project_urls                                       dict
@@ -2441,6 +2444,10 @@ requires                                           list-comma
 obsoletes                                          list-comma
 ==============================  =================  =====
 
+.. note::
+    A version loaded using the ``file:`` directive must comply with PEP 440.
+    It is easy to accidentally put something other than a valid version
+    string in such a file, so validation is stricter in this case.
 
 Options
 -------
diff --git a/netlify.toml b/netlify.toml
new file mode 100644 (file)
index 0000000..ec21e7b
--- /dev/null
@@ -0,0 +1,5 @@
+# Configuration for pull request documentation previews via Netlify
+
+[build]
+    publish = "docs/build/html"
+    command = "pip install tox && tox -e docs"
index d5b0fe9828107129709e19f303d2a7ad4c725669..4e4409b32b151e4f30a672ca5ff559d4d3640e69 100644 (file)
@@ -377,11 +377,7 @@ def get_build_platform():
     XXX Currently this is the same as ``distutils.util.get_platform()``, but it
     needs some hacks for Linux and Mac OS X.
     """
-    try:
-        # Python 2.7 or >=3.2
-        from sysconfig import get_platform
-    except ImportError:
-        from distutils.util import get_platform
+    from sysconfig import get_platform
 
     plat = get_platform()
     if sys.platform == "darwin" and not plat.startswith('macosx-'):
@@ -2667,6 +2663,19 @@ class Distribution(object):
             raise AttributeError(attr)
         return getattr(self._provider, attr)
 
+    def __dir__(self):
+        return list(
+            set(super(Distribution, self).__dir__())
+            | set(
+                attr for attr in self._provider.__dir__()
+                if not attr.startswith('_')
+            )
+        )
+
+    if not hasattr(object, '__dir__'):
+        # python 2.7 not supported
+        del __dir__
+
     @classmethod
     def from_filename(cls, filename, metadata=None, **kw):
         return cls.from_location(
index 05f35adef2a6726ff5f2fc1b9342faecc2a8802c..04d02c1f61817a32aa8f65a8487f1dd1bfc3623b 100644 (file)
@@ -145,6 +145,35 @@ class TestDistro:
         for v in "Twisted>=1.5", "Twisted>=1.5\nZConfig>=2.0":
             self.checkRequires(self.distRequires(v), v)
 
+    needs_object_dir = pytest.mark.skipif(
+        not hasattr(object, '__dir__'),
+        reason='object.__dir__ necessary for self.__dir__ implementation',
+    )
+
+    def test_distribution_dir(self):
+        d = pkg_resources.Distribution()
+        dir(d)
+
+    @needs_object_dir
+    def test_distribution_dir_includes_provider_dir(self):
+        d = pkg_resources.Distribution()
+        before = d.__dir__()
+        assert 'test_attr' not in before
+        d._provider.test_attr = None
+        after = d.__dir__()
+        assert len(after) == len(before) + 1
+        assert 'test_attr' in after
+
+    @needs_object_dir
+    def test_distribution_dir_ignores_provider_dir_leading_underscore(self):
+        d = pkg_resources.Distribution()
+        before = d.__dir__()
+        assert '_test_attr' not in before
+        d._provider._test_attr = None
+        after = d.__dir__()
+        assert len(after) == len(before)
+        assert '_test_attr' not in after
+
     def testResolve(self):
         ad = pkg_resources.Environment([])
         ws = WorkingSet([])
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644 (file)
index 0000000..cffd0e9
--- /dev/null
@@ -0,0 +1,34 @@
+[tool.towncrier]
+    package = "setuptools"
+    package_dir = "setuptools"
+    filename = "CHANGES.rst"
+    directory = "changelog.d"
+    title_format = "v{version}"
+    issue_format = "#{issue}"
+    template = "towncrier_template.rst"
+    underlines = ["-"]
+
+    [[tool.towncrier.type]]
+        directory = "deprecation"
+        name = "Deprecations"
+        showcontent = true
+
+    [[tool.towncrier.type]]
+        directory = "breaking"
+        name = "Breaking Changes"
+        showcontent = true
+
+    [[tool.towncrier.type]]
+        directory = "change"
+        name = "Changes"
+        showcontent = true
+
+    [[tool.towncrier.type]]
+        directory = "doc"
+        name = "Documentation changes"
+        showcontent = true
+
+    [[tool.towncrier.type]]
+        directory = "misc"
+        name = "Misc"
+        showcontent = true
index 2c84f0b7631c3c60a0654ba2eb091281a04087d8..e23ee6f041999d13268390526ee73e41ec87105e 100755 (executable)
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 39.1.0
+current_version = 39.2.0
 commit = True
 tag = True
 
index b08552d4376ae5737504fc5260afedf4ab9d6e04..b122df82f3289932a107df7b8045f113db803221 100755 (executable)
--- a/setup.py
+++ b/setup.py
@@ -89,7 +89,7 @@ def pypi_link(pkg_filename):
 
 setup_params = dict(
     name="setuptools",
-    version="39.1.0",
+    version="39.2.0",
     description=(
         "Easily download, build, install, upgrade, and uninstall "
         "Python packages"
index 8eddcae88acc86d274eb445dff6b8771ba6ecf86..d3f0b123d563d2e4a70f287745563796b2035aab 100644 (file)
@@ -7,6 +7,7 @@ from functools import partial
 from importlib import import_module
 
 from distutils.errors import DistutilsOptionError, DistutilsFileError
+from setuptools.extern.packaging.version import LegacyVersion, parse
 from setuptools.extern.six import string_types
 
 
@@ -101,14 +102,14 @@ def parse_configuration(
         If False exceptions are propagated as expected.
     :rtype: list
     """
-    meta = ConfigMetadataHandler(
-        distribution.metadata, command_options, ignore_option_errors)
-    meta.parse()
-
     options = ConfigOptionsHandler(
         distribution, command_options, ignore_option_errors)
     options.parse()
 
+    meta = ConfigMetadataHandler(
+        distribution.metadata, command_options, ignore_option_errors, distribution.package_dir)
+    meta.parse()
+
     return meta, options
 
 
@@ -280,7 +281,7 @@ class ConfigHandler(object):
             return f.read()
 
     @classmethod
-    def _parse_attr(cls, value):
+    def _parse_attr(cls, value, package_dir=None):
         """Represents value as a module attribute.
 
         Examples:
@@ -300,7 +301,21 @@ class ConfigHandler(object):
         module_name = '.'.join(attrs_path)
         module_name = module_name or '__init__'
 
-        sys.path.insert(0, os.getcwd())
+        parent_path = os.getcwd()
+        if package_dir:
+            if attrs_path[0] in package_dir:
+                # A custom path was specified for the module we want to import
+                custom_path = package_dir[attrs_path[0]]
+                parts = custom_path.rsplit('/', 1)
+                if len(parts) > 1:
+                    parent_path = os.path.join(os.getcwd(), parts[0])
+                    module_name = parts[1]
+                else:
+                    module_name = custom_path
+            elif '' in package_dir:
+                # A custom parent directory was specified for all root modules
+                parent_path = os.path.join(os.getcwd(), package_dir[''])
+        sys.path.insert(0, parent_path)
         try:
             module = import_module(module_name)
             value = getattr(module, attr_name)
@@ -399,6 +414,12 @@ class ConfigMetadataHandler(ConfigHandler):
 
     """
 
+    def __init__(self, target_obj, options, ignore_option_errors=False,
+                 package_dir=None):
+        super(ConfigMetadataHandler, self).__init__(target_obj, options,
+                                                    ignore_option_errors)
+        self.package_dir = package_dir
+
     @property
     def parsers(self):
         """Metadata item name to parser function mapping."""
@@ -427,7 +448,19 @@ class ConfigMetadataHandler(ConfigHandler):
         :rtype: str
 
         """
-        version = self._parse_attr(value)
+        version = self._parse_file(value)
+
+        if version != value:
+            version = version.strip()
+            # Be strict about versions loaded from file because it's easy to
+            # accidentally include newlines and other unintended content
+            if isinstance(parse(version), LegacyVersion):
+                raise DistutilsOptionError('Version loaded from %s does not comply with PEP 440: %s' % (
+                    value, version
+                ))
+            return version
+
+        version = self._parse_attr(value, self.package_dir)
 
         if callable(version):
             version = version()
index 321ab6b7d761e151e5148524766c15f52e381342..6ee4a97f3e71f14b1f65f9cd5e416e218eb6f773 100644 (file)
@@ -328,6 +328,12 @@ class Distribution(Distribution_parse_config_files, _Distribution):
     distribution for the included and excluded features.
     """
 
+    _DISTUTILS_UNSUPPORTED_METADATA = {
+        'long_description_content_type': None,
+        'project_urls': dict,
+        'provides_extras': set,
+    }
+
     _patched_dist = None
 
     def patch_missing_pkg_info(self, attrs):
@@ -353,25 +359,29 @@ class Distribution(Distribution_parse_config_files, _Distribution):
         self.require_features = []
         self.features = {}
         self.dist_files = []
+        # Filter-out setuptools' specific options.
         self.src_root = attrs.pop("src_root", None)
         self.patch_missing_pkg_info(attrs)
-        self.project_urls = attrs.get('project_urls', {})
         self.dependency_links = attrs.pop('dependency_links', [])
         self.setup_requires = attrs.pop('setup_requires', [])
         for ep in pkg_resources.iter_entry_points('distutils.setup_keywords'):
             vars(self).setdefault(ep.name, None)
-        _Distribution.__init__(self, attrs)
-
-        # The project_urls attribute may not be supported in distutils, so
-        # prime it here from our value if not automatically set
-        self.metadata.project_urls = getattr(
-            self.metadata, 'project_urls', self.project_urls)
-        self.metadata.long_description_content_type = attrs.get(
-            'long_description_content_type'
-        )
-        self.metadata.provides_extras = getattr(
-            self.metadata, 'provides_extras', set()
-        )
+        _Distribution.__init__(self, {
+            k: v for k, v in attrs.items()
+            if k not in self._DISTUTILS_UNSUPPORTED_METADATA
+        })
+
+        # Fill-in missing metadata fields not supported by distutils.
+        # Note some fields may have been set by other tools (e.g. pbr)
+        # above; they are taken preferrentially to setup() arguments
+        for option, default in self._DISTUTILS_UNSUPPORTED_METADATA.items():
+            for source in self.metadata.__dict__, attrs:
+                if option in source:
+                    value = source[option]
+                    break
+            else:
+                value = default() if default else None
+            setattr(self.metadata, option, value)
 
         if isinstance(self.metadata.version, numbers.Number):
             # Some people apparently take "version number" too literally :)
index abb953a8fa40547730d9ad2b98e0dfe8b0fe0a3b..19b376336366941b081b57577574deb08cc54362 100644 (file)
@@ -1,7 +1,8 @@
 import contextlib
 import pytest
 from distutils.errors import DistutilsOptionError, DistutilsFileError
-from setuptools.dist import Distribution
+from mock import patch
+from setuptools.dist import Distribution, _Distribution
 from setuptools.config import ConfigHandler, read_configuration
 
 
@@ -10,13 +11,15 @@ class ErrConfigHandler(ConfigHandler):
 
 
 def make_package_dir(name, base_dir):
-    dir_package = base_dir.mkdir(name)
+    dir_package = base_dir
+    for dir_name in name.split('/'):
+        dir_package = dir_package.mkdir(dir_name)
     init_file = dir_package.join('__init__.py')
     init_file.write('')
     return dir_package, init_file
 
 
-def fake_env(tmpdir, setup_cfg, setup_py=None):
+def fake_env(tmpdir, setup_cfg, setup_py=None, package_path='fake_package'):
 
     if setup_py is None:
         setup_py = (
@@ -28,7 +31,7 @@ def fake_env(tmpdir, setup_cfg, setup_py=None):
     config = tmpdir.join('setup.cfg')
     config.write(setup_cfg)
 
-    package_dir, init_file = make_package_dir('fake_package', tmpdir)
+    package_dir, init_file = make_package_dir(package_path, tmpdir)
 
     init_file.write(
         'VERSION = (1, 2, 3)\n'
@@ -268,6 +271,68 @@ class TestMetadata:
         with get_dist(tmpdir) as dist:
             assert dist.metadata.version == '2016.11.26'
 
+    def test_version_file(self, tmpdir):
+
+        _, config = fake_env(
+            tmpdir,
+            '[metadata]\n'
+            'version = file: fake_package/version.txt\n'
+        )
+        tmpdir.join('fake_package', 'version.txt').write('1.2.3\n')
+
+        with get_dist(tmpdir) as dist:
+            assert dist.metadata.version == '1.2.3'
+
+        tmpdir.join('fake_package', 'version.txt').write('1.2.3\n4.5.6\n')
+        with pytest.raises(DistutilsOptionError):
+            with get_dist(tmpdir) as dist:
+                _ = dist.metadata.version
+
+    def test_version_with_package_dir_simple(self, tmpdir):
+
+        _, config = fake_env(
+            tmpdir,
+            '[metadata]\n'
+            'version = attr: fake_package_simple.VERSION\n'
+            '[options]\n'
+            'package_dir =\n'
+            '    = src\n',
+            package_path='src/fake_package_simple'
+        )
+
+        with get_dist(tmpdir) as dist:
+            assert dist.metadata.version == '1.2.3'
+
+    def test_version_with_package_dir_rename(self, tmpdir):
+
+        _, config = fake_env(
+            tmpdir,
+            '[metadata]\n'
+            'version = attr: fake_package_rename.VERSION\n'
+            '[options]\n'
+            'package_dir =\n'
+            '    fake_package_rename = fake_dir\n',
+            package_path='fake_dir'
+        )
+
+        with get_dist(tmpdir) as dist:
+            assert dist.metadata.version == '1.2.3'
+
+    def test_version_with_package_dir_complex(self, tmpdir):
+
+        _, config = fake_env(
+            tmpdir,
+            '[metadata]\n'
+            'version = attr: fake_package_complex.VERSION\n'
+            '[options]\n'
+            'package_dir =\n'
+            '    fake_package_complex = src/fake_dir\n',
+            package_path='src/fake_dir'
+        )
+
+        with get_dist(tmpdir) as dist:
+            assert dist.metadata.version == '1.2.3'
+
     def test_unknown_meta_item(self, tmpdir):
 
         fake_env(
@@ -581,3 +646,43 @@ class TestOptions:
 
         with get_dist(tmpdir) as dist:
             assert dist.entry_points == expected
+
+saved_dist_init = _Distribution.__init__
+class TestExternalSetters:
+    # During creation of the setuptools Distribution() object, we call
+    # the init of the parent distutils Distribution object via
+    # _Distribution.__init__ ().
+    #
+    # It's possible distutils calls out to various keyword
+    # implementations (i.e. distutils.setup_keywords entry points)
+    # that may set a range of variables.
+    #
+    # This wraps distutil's Distribution.__init__ and simulates
+    # pbr or something else setting these values.
+    def _fake_distribution_init(self, dist, attrs):
+        saved_dist_init(dist, attrs)
+        # see self._DISTUTUILS_UNSUPPORTED_METADATA
+        setattr(dist.metadata, 'long_description_content_type',
+                'text/something')
+        # Test overwrite setup() args
+        setattr(dist.metadata, 'project_urls', {
+            'Link One': 'https://example.com/one/',
+            'Link Two': 'https://example.com/two/',
+        })
+        return None
+
+    @patch.object(_Distribution, '__init__', autospec=True)
+    def test_external_setters(self,  mock_parent_init, tmpdir):
+        mock_parent_init.side_effect = self._fake_distribution_init
+
+        dist = Distribution(attrs={
+            'project_urls': {
+                'will_be': 'ignored'
+            }
+        })
+
+        assert dist.metadata.long_description_content_type == 'text/something'
+        assert dist.metadata.project_urls == {
+            'Link One': 'https://example.com/one/',
+            'Link Two': 'https://example.com/two/',
+        }
index 2a070debe0b93e3e1c0cc53e21f1b0ae69fa7963..8b3b90f77f4c423cb5e9f52fb437d5edf23a0359 100644 (file)
@@ -128,11 +128,11 @@ class TestEggInfo(object):
 
         self._validate_content_order(content, expected_order)
 
-    def test_egg_base_installed_egg_info(self, tmpdir_cwd, env):
+    def test_expected_files_produced(self, tmpdir_cwd, env):
         self._create_project()
 
-        self._run_install_command(tmpdir_cwd, env)
-        actual = self._find_egg_info_files(env.paths['lib'])
+        self._run_egg_info_command(tmpdir_cwd, env)
+        actual = os.listdir('foo.egg-info')
 
         expected = [
             'PKG-INFO',
@@ -154,8 +154,8 @@ class TestEggInfo(object):
                 'usage.rst': "Run 'hi'",
             }
         })
-        self._run_install_command(tmpdir_cwd, env)
-        egg_info_dir = self._find_egg_info_files(env.paths['lib']).base
+        self._run_egg_info_command(tmpdir_cwd, env)
+        egg_info_dir = os.path.join('.', 'foo.egg-info')
         sources_txt = os.path.join(egg_info_dir, 'SOURCES.txt')
         with open(sources_txt) as f:
             assert 'docs/usage.rst' in f.read().split('\n')
@@ -233,27 +233,27 @@ class TestEggInfo(object):
         '''
         install_requires_deterministic
 
-        install_requires=["fake-factory==0.5.2", "pytz"]
+        install_requires=["wheel>=0.5", "pytest"]
 
         [options]
         install_requires =
-            fake-factory==0.5.2
-            pytz
+            wheel>=0.5
+            pytest
 
-        fake-factory==0.5.2
-        pytz
+        wheel>=0.5
+        pytest
         ''',
 
         '''
         install_requires_ordered
 
-        install_requires=["fake-factory>=1.12.3,!=2.0"]
+        install_requires=["pytest>=3.0.2,!=10.9999"]
 
         [options]
         install_requires =
-            fake-factory>=1.12.3,!=2.0
+            pytest>=3.0.2,!=10.9999
 
-        fake-factory!=2.0,>=1.12.3
+        pytest!=10.9999,>=3.0.2
         ''',
 
         '''
@@ -394,7 +394,7 @@ class TestEggInfo(object):
             self, tmpdir_cwd, env, requires, use_setup_cfg,
             expected_requires, install_cmd_kwargs):
         self._setup_script_with_requires(requires, use_setup_cfg)
-        self._run_install_command(tmpdir_cwd, env, **install_cmd_kwargs)
+        self._run_egg_info_command(tmpdir_cwd, env, **install_cmd_kwargs)
         egg_info_dir = os.path.join('.', 'foo.egg-info')
         requires_txt = os.path.join(egg_info_dir, 'requires.txt')
         if os.path.exists(requires_txt):
@@ -414,14 +414,14 @@ class TestEggInfo(object):
         req = 'install_requires={"fake-factory==0.5.2", "pytz"}'
         self._setup_script_with_requires(req)
         with pytest.raises(AssertionError):
-            self._run_install_command(tmpdir_cwd, env)
+            self._run_egg_info_command(tmpdir_cwd, env)
 
     def test_extras_require_with_invalid_marker(self, tmpdir_cwd, env):
         tmpl = 'extras_require={{":{marker}": ["barbazquux"]}},'
         req = tmpl.format(marker=self.invalid_marker)
         self._setup_script_with_requires(req)
         with pytest.raises(AssertionError):
-            self._run_install_command(tmpdir_cwd, env)
+            self._run_egg_info_command(tmpdir_cwd, env)
         assert glob.glob(os.path.join(env.paths['lib'], 'barbazquux*')) == []
 
     def test_extras_require_with_invalid_marker_in_req(self, tmpdir_cwd, env):
@@ -429,7 +429,7 @@ class TestEggInfo(object):
         req = tmpl.format(marker=self.invalid_marker)
         self._setup_script_with_requires(req)
         with pytest.raises(AssertionError):
-            self._run_install_command(tmpdir_cwd, env)
+            self._run_egg_info_command(tmpdir_cwd, env)
         assert glob.glob(os.path.join(env.paths['lib'], 'barbazquux*')) == []
 
     def test_provides_extra(self, tmpdir_cwd, env):
@@ -541,15 +541,6 @@ class TestEggInfo(object):
         assert 'Requires-Python: >=2.7.12' in pkg_info_lines
         assert 'Metadata-Version: 1.2' in pkg_info_lines
 
-    def test_python_requires_install(self, tmpdir_cwd, env):
-        self._setup_script_with_requires(
-            """python_requires='>=1.2.3',""")
-        self._run_install_command(tmpdir_cwd, env)
-        egg_info_dir = self._find_egg_info_files(env.paths['lib']).base
-        pkginfo = os.path.join(egg_info_dir, 'PKG-INFO')
-        with open(pkginfo) as f:
-            assert 'Requires-Python: >=1.2.3' in f.read().split('\n')
-
     def test_manifest_maker_warning_suppression(self):
         fixtures = [
             "standard file not found: should have one of foo.py, bar.py",
@@ -559,17 +550,13 @@ class TestEggInfo(object):
         for msg in fixtures:
             assert manifest_maker._should_suppress_warning(msg)
 
-    def _run_install_command(self, tmpdir_cwd, env, cmd=None, output=None):
+    def _run_egg_info_command(self, tmpdir_cwd, env, cmd=None, output=None):
         environ = os.environ.copy().update(
             HOME=env.paths['home'],
         )
         if cmd is None:
             cmd = [
-                'install',
-                '--home', env.paths['home'],
-                '--install-lib', env.paths['lib'],
-                '--install-scripts', env.paths['scripts'],
-                '--install-data', env.paths['data'],
+                'egg_info',
             ]
         code, data = environment.run_setup_py(
             cmd=cmd,
@@ -581,18 +568,3 @@ class TestEggInfo(object):
             raise AssertionError(data)
         if output:
             assert output in data
-
-    def _find_egg_info_files(self, root):
-        class DirList(list):
-            def __init__(self, files, base):
-                super(DirList, self).__init__(files)
-                self.base = base
-
-        results = (
-            DirList(filenames, dirpath)
-            for dirpath, dirnames, filenames in os.walk(root)
-            if os.path.basename(dirpath) == 'EGG-INFO'
-        )
-        # expect exactly one result
-        result, = results
-        return result
diff --git a/setuptools/tests/test_glibc.py b/setuptools/tests/test_glibc.py
new file mode 100644 (file)
index 0000000..9cb9796
--- /dev/null
@@ -0,0 +1,37 @@
+import warnings
+
+from setuptools.glibc import check_glibc_version
+
+
+class TestGlibc(object):
+    def test_manylinux1_check_glibc_version(self):
+        """
+        Test that the check_glibc_version function is robust against weird
+        glibc version strings.
+        """
+        for two_twenty in ["2.20",
+                           # used by "linaro glibc", see gh-3588
+                           "2.20-2014.11",
+                           # weird possibilities that I just made up
+                           "2.20+dev",
+                           "2.20-custom",
+                           "2.20.1",
+                           ]:
+            assert check_glibc_version(two_twenty, 2, 15)
+            assert check_glibc_version(two_twenty, 2, 20)
+            assert not check_glibc_version(two_twenty, 2, 21)
+            assert not check_glibc_version(two_twenty, 3, 15)
+            assert not check_glibc_version(two_twenty, 1, 15)
+
+        # For strings that we just can't parse at all, we should warn and
+        # return false
+        for bad_string in ["asdf", "", "foo.bar"]:
+            with warnings.catch_warnings(record=True) as ws:
+                warnings.filterwarnings("always")
+                assert not check_glibc_version(bad_string, 2, 5)
+                for w in ws:
+                    if "Expected glibc version with" in str(w.message):
+                        break
+                else:
+                    # Didn't find the warning we were expecting
+                    assert False
diff --git a/setuptools/tests/test_pep425tags.py b/setuptools/tests/test_pep425tags.py
new file mode 100644 (file)
index 0000000..0f60e0e
--- /dev/null
@@ -0,0 +1,164 @@
+import sys
+
+from mock import patch
+
+from setuptools import pep425tags
+
+
+class TestPEP425Tags(object):
+
+    def mock_get_config_var(self, **kwd):
+        """
+        Patch sysconfig.get_config_var for arbitrary keys.
+        """
+        get_config_var = pep425tags.sysconfig.get_config_var
+
+        def _mock_get_config_var(var):
+            if var in kwd:
+                return kwd[var]
+            return get_config_var(var)
+        return _mock_get_config_var
+
+    def abi_tag_unicode(self, flags, config_vars):
+        """
+        Used to test ABI tags, verify correct use of the `u` flag
+        """
+        config_vars.update({'SOABI': None})
+        base = pep425tags.get_abbr_impl() + pep425tags.get_impl_ver()
+
+        if sys.version_info < (3, 3):
+            config_vars.update({'Py_UNICODE_SIZE': 2})
+            mock_gcf = self.mock_get_config_var(**config_vars)
+            with patch('setuptools.pep425tags.sysconfig.get_config_var', mock_gcf):
+                abi_tag = pep425tags.get_abi_tag()
+                assert abi_tag == base + flags
+
+            config_vars.update({'Py_UNICODE_SIZE': 4})
+            mock_gcf = self.mock_get_config_var(**config_vars)
+            with patch('setuptools.pep425tags.sysconfig.get_config_var',
+                       mock_gcf):
+                abi_tag = pep425tags.get_abi_tag()
+                assert abi_tag == base + flags + 'u'
+
+        else:
+            # On Python >= 3.3, UCS-4 is essentially permanently enabled, and
+            # Py_UNICODE_SIZE is None. SOABI on these builds does not include
+            # the 'u' so manual SOABI detection should not do so either.
+            config_vars.update({'Py_UNICODE_SIZE': None})
+            mock_gcf = self.mock_get_config_var(**config_vars)
+            with patch('setuptools.pep425tags.sysconfig.get_config_var',
+                       mock_gcf):
+                abi_tag = pep425tags.get_abi_tag()
+                assert abi_tag == base + flags
+
+    def test_broken_sysconfig(self):
+        """
+        Test that pep425tags still works when sysconfig is broken.
+        Can be a problem on Python 2.7
+        Issue #1074.
+        """
+        def raises_ioerror(var):
+            raise IOError("I have the wrong path!")
+
+        with patch('setuptools.pep425tags.sysconfig.get_config_var',
+                   raises_ioerror):
+            assert len(pep425tags.get_supported())
+
+    def test_no_hyphen_tag(self):
+        """
+        Test that no tag contains a hyphen.
+        """
+        mock_gcf = self.mock_get_config_var(SOABI='cpython-35m-darwin')
+
+        with patch('setuptools.pep425tags.sysconfig.get_config_var',
+                   mock_gcf):
+            supported = pep425tags.get_supported()
+
+        for (py, abi, plat) in supported:
+            assert '-' not in py
+            assert '-' not in abi
+            assert '-' not in plat
+
+    def test_manual_abi_noflags(self):
+        """
+        Test that no flags are set on a non-PyDebug, non-Pymalloc ABI tag.
+        """
+        self.abi_tag_unicode('', {'Py_DEBUG': False, 'WITH_PYMALLOC': False})
+
+    def test_manual_abi_d_flag(self):
+        """
+        Test that the `d` flag is set on a PyDebug, non-Pymalloc ABI tag.
+        """
+        self.abi_tag_unicode('d', {'Py_DEBUG': True, 'WITH_PYMALLOC': False})
+
+    def test_manual_abi_m_flag(self):
+        """
+        Test that the `m` flag is set on a non-PyDebug, Pymalloc ABI tag.
+        """
+        self.abi_tag_unicode('m', {'Py_DEBUG': False, 'WITH_PYMALLOC': True})
+
+    def test_manual_abi_dm_flags(self):
+        """
+        Test that the `dm` flags are set on a PyDebug, Pymalloc ABI tag.
+        """
+        self.abi_tag_unicode('dm', {'Py_DEBUG': True, 'WITH_PYMALLOC': True})
+
+
+class TestManylinux1Tags(object):
+
+    @patch('setuptools.pep425tags.get_platform', lambda: 'linux_x86_64')
+    @patch('setuptools.glibc.have_compatible_glibc',
+           lambda major, minor: True)
+    def test_manylinux1_compatible_on_linux_x86_64(self):
+        """
+        Test that manylinux1 is enabled on linux_x86_64
+        """
+        assert pep425tags.is_manylinux1_compatible()
+
+    @patch('setuptools.pep425tags.get_platform', lambda: 'linux_i686')
+    @patch('setuptools.glibc.have_compatible_glibc',
+           lambda major, minor: True)
+    def test_manylinux1_compatible_on_linux_i686(self):
+        """
+        Test that manylinux1 is enabled on linux_i686
+        """
+        assert pep425tags.is_manylinux1_compatible()
+
+    @patch('setuptools.pep425tags.get_platform', lambda: 'linux_x86_64')
+    @patch('setuptools.glibc.have_compatible_glibc',
+           lambda major, minor: False)
+    def test_manylinux1_2(self):
+        """
+        Test that manylinux1 is disabled with incompatible glibc
+        """
+        assert not pep425tags.is_manylinux1_compatible()
+
+    @patch('setuptools.pep425tags.get_platform', lambda: 'arm6vl')
+    @patch('setuptools.glibc.have_compatible_glibc',
+           lambda major, minor: True)
+    def test_manylinux1_3(self):
+        """
+        Test that manylinux1 is disabled on arm6vl
+        """
+        assert not pep425tags.is_manylinux1_compatible()
+
+    @patch('setuptools.pep425tags.get_platform', lambda: 'linux_x86_64')
+    @patch('setuptools.glibc.have_compatible_glibc',
+           lambda major, minor: True)
+    @patch('sys.platform', 'linux2')
+    def test_manylinux1_tag_is_first(self):
+        """
+        Test that the more specific tag manylinux1 comes first.
+        """
+        groups = {}
+        for pyimpl, abi, arch in pep425tags.get_supported():
+            groups.setdefault((pyimpl, abi), []).append(arch)
+
+        for arches in groups.values():
+            if arches == ['any']:
+                continue
+            # Expect the most specific arch first:
+            if len(arches) == 3:
+                assert arches == ['manylinux1_x86_64', 'linux_x86_64', 'any']
+            else:
+                assert arches == ['manylinux1_x86_64', 'linux_x86_64']
index 150ac4c1b5e6434284fb98394a3e72b7f180e30c..cf6508686d78df1a1c9ff974316476948d28422b 100644 (file)
@@ -9,12 +9,15 @@ import contextlib
 import glob
 import inspect
 import os
+import shutil
 import subprocess
 import sys
+import zipfile
 
 import pytest
 
 from pkg_resources import Distribution, PathMetadata, PY_MAJOR
+from setuptools.extern.packaging.utils import canonicalize_name
 from setuptools.wheel import Wheel
 
 from .contexts import tempdir
@@ -506,3 +509,33 @@ def test_wheel_install(params):
         _check_wheel_install(filename, install_dir,
                              install_tree, project_name,
                              version, requires_txt)
+
+
+def test_wheel_install_pep_503():
+    project_name = 'Foo_Bar'    # PEP 503 canonicalized name is "foo-bar"
+    version = '1.0'
+    with build_wheel(
+        name=project_name,
+        version=version,
+    ) as filename, tempdir() as install_dir:
+        new_filename = filename.replace(project_name,
+                                        canonicalize_name(project_name))
+        shutil.move(filename, new_filename)
+        _check_wheel_install(new_filename, install_dir, None,
+                             canonicalize_name(project_name),
+                             version, None)
+
+
+def test_wheel_no_dist_dir():
+    project_name = 'nodistinfo'
+    version = '1.0'
+    wheel_name = '{0}-{1}-py2.py3-none-any.whl'.format(project_name, version)
+    with tempdir() as source_dir:
+        wheel_path = os.path.join(source_dir, wheel_name)
+        # create an empty zip file
+        zipfile.ZipFile(wheel_path, 'w').close()
+        with tempdir() as install_dir:
+            with pytest.raises(ValueError):
+                _check_wheel_install(wheel_path, install_dir, None,
+                                     project_name,
+                                     version, None)
index 37dfa53103ec1261b2b7c8c0c8d72994eaf2d984..4a33b20324a0d9b8b030dc2b55f8f213af0c0c3f 100644 (file)
@@ -4,10 +4,12 @@ from distutils.util import get_platform
 import email
 import itertools
 import os
+import posixpath
 import re
 import zipfile
 
 from pkg_resources import Distribution, PathMetadata, parse_version
+from setuptools.extern.packaging.utils import canonicalize_name
 from setuptools.extern.six import PY3
 from setuptools import Distribution as SetuptoolsDistribution
 from setuptools import pep425tags
@@ -77,14 +79,24 @@ class Wheel(object):
             platform=(None if self.platform == 'any' else get_platform()),
         ).egg_name() + '.egg'
 
+    def get_dist_info(self, zf):
+        # find the correct name of the .dist-info dir in the wheel file
+        for member in zf.namelist():
+            dirname = posixpath.dirname(member)
+            if (dirname.endswith('.dist-info') and
+                    canonicalize_name(dirname).startswith(
+                        canonicalize_name(self.project_name))):
+                return dirname
+        raise ValueError("unsupported wheel format. .dist-info not found")
+
     def install_as_egg(self, destination_eggdir):
         '''Install wheel as an egg directory.'''
         with zipfile.ZipFile(self.filename) as zf:
             dist_basename = '%s-%s' % (self.project_name, self.version)
-            dist_info = '%s.dist-info' % dist_basename
+            dist_info = self.get_dist_info(zf)
             dist_data = '%s.data' % dist_basename
             def get_metadata(name):
-                with zf.open('%s/%s' % (dist_info, name)) as fp:
+                with zf.open(posixpath.join(dist_info, name)) as fp:
                     value = fp.read().decode('utf-8') if PY3 else fp.read()
                     return email.parser.Parser().parsestr(value)
             wheel_metadata = get_metadata('WHEEL')
index fd826d0974bf084d7d2e8d48d01ad34d8b8ea968..aff32c10eab239b053bb32b1211ffb013641fad7 100644 (file)
@@ -1,6 +1,7 @@
 importlib; python_version<"2.7"
 mock
-pytest-flake8; python_version>="2.7"
+pytest-flake8<=1.0.0; python_version>="3.3" and python_version<"3.5"
+pytest-flake8; python_version>="2.7" and python_version!="3.3" and python_version!="3.4"
 virtualenv>=13.0.0
 pytest-virtualenv>=1.2.7
 pytest>=3.0.2
diff --git a/towncrier_template.rst b/towncrier_template.rst
new file mode 100644 (file)
index 0000000..9c23b97
--- /dev/null
@@ -0,0 +1,26 @@
+{% for section, _ in sections.items() %}
+{% set underline = underlines[0] %}{% if section %}{{section}}
+{{ underline * section|length }}
+{% endif %}
+{% if sections[section] %}
+{% for category, val in definitions.items() if category in sections[section]%}
+{% if definitions[category]['showcontent'] %}
+{% for text, values in sections[section][category].items() %}
+* {{ values|join(', ') }}: {{ text }}
+{% endfor %}
+{% else %}
+*  {{ sections[section][category]['']|join(', ') }}
+
+{% endif %}
+{% if sections[section][category]|length == 0 %}
+No significant changes.
+{% else %}
+{% endif %}
+{% endfor %}
+
+{% else %}
+No significant changes.
+
+
+{% endif %}
+{% endfor %}
diff --git a/tox.ini b/tox.ini
index a0c4cdf3000f4d10835aa11d4057112ff4551308..a16e89faa727fd4f644c66bc9ae983a2e31780b6 100644 (file)
--- a/tox.ini
+++ b/tox.ini
@@ -33,6 +33,14 @@ deps=codecov
 skip_install=True
 commands=codecov --file {toxworkdir}/coverage.xml
 
+[testenv:docs]
+deps = -r{toxinidir}/docs/requirements.txt
+skip_install=True
+commands =
+    python {toxinidir}/bootstrap.py
+    sphinx-build -W -b html -d {envtmpdir}/doctrees docs docs/build/html
+    sphinx-build -W -b man -d {envtmpdir}/doctrees docs docs/build/man
+
 [coverage:run]
 source=
        pkg_resources