[bumpversion]
-current_version = 41.0.1
+current_version = 41.1.0
commit = True
tag = True
python: 2.7
- <<: *latest_py2
env: LANG=C
- - python: pypy2.7-6.0.0
+ - python: pypy
env: DISABLE_COVERAGE=1 # Don't run coverage on pypy (too slow).
- - python: pypy3.5-6.0.0
+ - python: pypy3
env: DISABLE_COVERAGE=1
- python: 3.4
- python: 3.5
+v41.1.0
+-------
+
+* #1697: Moved most of the constants from setup.py to setup.cfg
+* #1749: Fixed issue with the PEP 517 backend where building a source distribution would fail if any tarball existed in the destination directory.
+* #1750: Fixed an issue with PEP 517 backend where wheel builds would fail if the destination directory did not already exist.
+* #1756: Forse metadata-version >= 1.2. when project urls are present.
+* #1769: Improve ``package_data`` check: ensure the dictionary values are lists/tuples of strings.
+* #1788: Changed compatibility fallback logic for ``html.unescape`` to avoid accessing ``HTMLParser.unescape`` when not necessary. ``HTMLParser.unescape`` is deprecated and will be removed in Python 3.9.
+* #1790: Added the file path to the error message when a ``UnicodeDecodeError`` occurs while reading a metadata file.
+* #1776: Use license classifiers rather than the license field.
+
+
v41.0.1
-------
test_script:
- python --version
- python -m pip install --disable-pip-version-check --upgrade pip setuptools wheel
- - pip install --upgrade tox tox-venv
+ - pip install --upgrade tox tox-venv virtualenv
- pip freeze --all
- python bootstrap.py
- tox -- --cov
.. _Sphinx: http://www.sphinx-doc.org/en/master/
.. _published documentation: https://setuptools.readthedocs.io/en/latest/
+
+---------------------
+Vendored Dependencies
+---------------------
+
+Setuptools has some dependencies, but due to `bootstrapping issues
+<https://github.com/pypa/setuptools/issues/980>`, those dependencies
+cannot be declared as they won't be resolved soon enough to build
+setuptools from source. Eventually, this limitation may be lifted as
+PEP 517/518 reach ubiquitous adoption, but for now, Setuptools
+cannot declare dependencies other than through
+``setuptools/_vendor/vendored.txt`` and
+``pkg_reosurces/_vendor/vendored.txt`` and refreshed by way of
+``paver update_vendored`` (pavement.py).
author="Me",
author_email="me@example.com",
description="This is an Example Package",
- license="PSF",
keywords="hello world example examples",
url="http://example.com/HelloWorld/", # project home page, if any
project_urls={
"Bug Tracker": "https://bugs.example.com/HelloWorld/",
"Documentation": "https://docs.example.com/HelloWorld/",
"Source Code": "https://code.example.com/HelloWorld/",
- }
+ },
+ classifiers=[
+ 'License :: OSI Approved :: Python Software Foundation License'
+ ]
- # could also include long_description, download_url, classifiers, etc.
+ # could also include long_description, download_url, etc.
)
In the sections that follow, we'll explain what most of these ``setup()``
license = BSD 3-Clause License
classifiers =
Framework :: Django
+ License :: OSI Approved :: BSD License
Programming Language :: Python :: 3
Programming Language :: Python :: 3.5
def get_metadata(self, name):
if not self.egg_info:
return ""
- value = self._get(self._fn(self.egg_info, name))
- return value.decode('utf-8') if six.PY3 else value
+ path = self._get_metadata_path(name)
+ value = self._get(path)
+ if six.PY2:
+ return value
+ try:
+ return value.decode('utf-8')
+ except UnicodeDecodeError as exc:
+ # Include the path in the error message to simplify
+ # troubleshooting, and without changing the exception type.
+ exc.reason += ' in {} file at path: {}'.format(name, path)
+ raise
def get_metadata_lines(self, name):
return yield_lines(self.get_metadata(name))
import mock
from pkg_resources import DistInfoDistribution, Distribution, EggInfoDistribution
+from setuptools.extern import six
from pkg_resources.extern.six.moves import map
from pkg_resources.extern.six import text_type, string_types
subprocess.check_call(cmd)
+def make_test_distribution(metadata_path, metadata):
+ """
+ Make a test Distribution object, and return it.
+
+ :param metadata_path: the path to the metadata file that should be
+ created. This should be inside a distribution directory that should
+ also be created. For example, an argument value might end with
+ "<project>.dist-info/METADATA".
+ :param metadata: the desired contents of the metadata file, as bytes.
+ """
+ dist_dir = os.path.dirname(metadata_path)
+ os.mkdir(dist_dir)
+ with open(metadata_path, 'wb') as f:
+ f.write(metadata)
+ dists = list(pkg_resources.distributions_from_metadata(dist_dir))
+ dist, = dists
+
+ return dist
+
+
+def test_get_metadata__bad_utf8(tmpdir):
+ """
+ Test a metadata file with bytes that can't be decoded as utf-8.
+ """
+ filename = 'METADATA'
+ # Convert the tmpdir LocalPath object to a string before joining.
+ metadata_path = os.path.join(str(tmpdir), 'foo.dist-info', filename)
+ # Encode a non-ascii string with the wrong encoding (not utf-8).
+ metadata = 'née'.encode('iso-8859-1')
+ dist = make_test_distribution(metadata_path, metadata=metadata)
+
+ if six.PY2:
+ # In Python 2, get_metadata() doesn't do any decoding.
+ actual = dist.get_metadata(filename)
+ assert actual == metadata
+ return
+
+ # Otherwise, we are in the Python 3 case.
+ with pytest.raises(UnicodeDecodeError) as excinfo:
+ dist.get_metadata(filename)
+
+ exc = excinfo.value
+ actual = str(exc)
+ expected = (
+ # The error message starts with "'utf-8' codec ..." However, the
+ # spelling of "utf-8" can vary (e.g. "utf8") so we don't include it
+ "codec can't decode byte 0xe9 in position 1: "
+ 'invalid continuation byte in METADATA file at path: '
+ )
+ assert expected in actual, 'actual: {}'.format(actual)
+ assert actual.endswith(metadata_path), 'actual: {}'.format(actual)
+
+
# TODO: remove this in favor of Path.touch() when Python 2 is dropped.
def touch_file(path):
"""
with pytest.raises(ValueError) as excinfo:
dist.version
- err = str(excinfo)
+ err = str(excinfo.value)
# Include a string expression after the assert so the full strings
# will be visible for inspection on failure.
assert expected_text in err, str((expected_text, err))
[pytest]
-addopts=--doctest-modules --doctest-glob=pkg_resources/api_tests.txt -rsxX
+addopts=--doctest-modules --doctest-glob=pkg_resources/api_tests.txt -r sxX
norecursedirs=dist build *.egg setuptools/extern pkg_resources/extern .*
flake8-ignore =
setuptools/site-patch.py F821
universal = 1
[metadata]
+name = setuptools
+version = 41.1.0
+description = Easily download, build, install, upgrade, and uninstall Python packages
+author = Python Packaging Authority
+author_email = distutils-sig@python.org
+long_description = file: README.rst
+long_description_content_type = text/x-rst; charset=UTF-8
license_file = LICENSE
-version = 41.0.1
+keywords = CPAN PyPI distutils eggs package management
+url = https://github.com/pypa/setuptools
+project_urls =
+ Documentation = https://setuptools.readthedocs.io/
+classifiers =
+ Development Status :: 5 - Production/Stable
+ Intended Audience :: Developers
+ License :: OSI Approved :: MIT License
+ Operating System :: OS Independent
+ Programming Language :: Python :: 2
+ Programming Language :: Python :: 2.7
+ Programming Language :: Python :: 3
+ Programming Language :: Python :: 3.4
+ Programming Language :: Python :: 3.5
+ Programming Language :: Python :: 3.6
+ Programming Language :: Python :: 3.7
+ Topic :: Software Development :: Libraries :: Python Modules
+ Topic :: System :: Archiving :: Packaging
+ Topic :: System :: Systems Administration
+ Topic :: Utilities
+
+[options]
+zip_safe = True
+python_requires = >=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*
+py_modules = easy_install
+packages = find:
+
+[options.packages.find]
+exclude = *.tests
+
+[options.extras_require]
+ssl =
+ wincertstore==0.2; sys_platform=='win32'
+certs =
+ certifi==2016.9.26
Distutils setup file, used to install or test 'setuptools'
"""
-import io
import os
import sys
-import textwrap
import setuptools
yield tmpl.format(shortver=sys.version[:3])
-readme_path = os.path.join(here, 'README.rst')
-with io.open(readme_path, encoding='utf-8') as readme_file:
- long_description = readme_file.read()
-
package_data = dict(
setuptools=['script (dev).tmpl', 'script.tmpl', 'site-patch.py'],
)
setup_params = dict(
- name="setuptools",
- description=(
- "Easily download, build, install, upgrade, and uninstall "
- "Python packages"
- ),
- author="Python Packaging Authority",
- author_email="distutils-sig@python.org",
- long_description=long_description,
- long_description_content_type='text/x-rst; charset=UTF-8',
- keywords="CPAN PyPI distutils eggs package management",
- url="https://github.com/pypa/setuptools",
- project_urls={
- "Documentation": "https://setuptools.readthedocs.io/",
- },
src_root=None,
- packages=setuptools.find_packages(exclude=['*.tests']),
package_data=package_data,
- py_modules=['easy_install'],
- zip_safe=True,
entry_points={
"distutils.commands": [
"%(cmd)s = setuptools.command.%(cmd)s:%(cmd)s" % locals()
"setuptools.installation":
['eggsecutable = setuptools.command.easy_install:bootstrap'],
},
- classifiers=textwrap.dedent("""
- Development Status :: 5 - Production/Stable
- Intended Audience :: Developers
- License :: OSI Approved :: MIT License
- Operating System :: OS Independent
- Programming Language :: Python :: 2
- Programming Language :: Python :: 2.7
- Programming Language :: Python :: 3
- Programming Language :: Python :: 3.4
- Programming Language :: Python :: 3.5
- Programming Language :: Python :: 3.6
- Programming Language :: Python :: 3.7
- Topic :: Software Development :: Libraries :: Python Modules
- Topic :: System :: Archiving :: Packaging
- Topic :: System :: Systems Administration
- Topic :: Utilities
- """).strip().splitlines(),
- python_requires='>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*',
- extras_require={
- "ssl:sys_platform=='win32'": "wincertstore==0.2",
- "certs": "certifi==2016.9.26",
- },
dependency_links=[
pypi_link(
'certifi-2016.9.26.tar.gz#md5=baa81e951a29958563689d868ef1064d',
'wincertstore-0.2.zip#md5=ae728f2f007185648d0c7a8679b361e2',
),
],
- scripts=[],
setup_requires=[
] + wheel,
)
from setuptools.py31compat import TemporaryDirectory
from pkg_resources import parse_requirements
+from pkg_resources.py31compat import makedirs
__all__ = ['get_requires_for_build_sdist',
'get_requires_for_build_wheel',
return dist_infos[0]
- def build_wheel(self, wheel_directory, config_settings=None,
- metadata_directory=None):
+ def _build_with_temp_dir(self, setup_command, result_extension,
+ result_directory, config_settings):
config_settings = self._fix_config(config_settings)
- wheel_directory = os.path.abspath(wheel_directory)
+ result_directory = os.path.abspath(result_directory)
- # Build the wheel in a temporary directory, then copy to the target
- with TemporaryDirectory(dir=wheel_directory) as tmp_dist_dir:
- sys.argv = (sys.argv[:1] +
- ['bdist_wheel', '--dist-dir', tmp_dist_dir] +
+ # Build in a temporary directory, then copy to the target.
+ makedirs(result_directory, exist_ok=True)
+ with TemporaryDirectory(dir=result_directory) as tmp_dist_dir:
+ sys.argv = (sys.argv[:1] + setup_command +
+ ['--dist-dir', tmp_dist_dir] +
config_settings["--global-option"])
self.run_setup()
- wheel_basename = _file_with_extension(tmp_dist_dir, '.whl')
- wheel_path = os.path.join(wheel_directory, wheel_basename)
- if os.path.exists(wheel_path):
- # os.rename will fail overwriting on non-unix env
- os.remove(wheel_path)
- os.rename(os.path.join(tmp_dist_dir, wheel_basename), wheel_path)
+ result_basename = _file_with_extension(tmp_dist_dir, result_extension)
+ result_path = os.path.join(result_directory, result_basename)
+ if os.path.exists(result_path):
+ # os.rename will fail overwriting on non-Unix.
+ os.remove(result_path)
+ os.rename(os.path.join(tmp_dist_dir, result_basename), result_path)
- return wheel_basename
+ return result_basename
- def build_sdist(self, sdist_directory, config_settings=None):
- config_settings = self._fix_config(config_settings)
- sdist_directory = os.path.abspath(sdist_directory)
- sys.argv = sys.argv[:1] + ['sdist', '--formats', 'gztar'] + \
- config_settings["--global-option"] + \
- ["--dist-dir", sdist_directory]
- self.run_setup()
- return _file_with_extension(sdist_directory, '.tar.gz')
+ def build_wheel(self, wheel_directory, config_settings=None,
+ metadata_directory=None):
+ return self._build_with_temp_dir(['bdist_wheel'], '.whl',
+ wheel_directory, config_settings)
+
+ def build_sdist(self, sdist_directory, config_settings=None):
+ return self._build_with_temp_dir(['sdist', '--formats', 'gztar'],
+ '.tar.gz', sdist_directory,
+ config_settings)
class _BuildMetaLegacyBackend(_BuildMetaBackend):
mv = StrictVersion('2.1')
elif (self.maintainer is not None or
self.maintainer_email is not None or
- getattr(self, 'python_requires', None) is not None):
+ getattr(self, 'python_requires', None) is not None or
+ self.project_urls):
mv = StrictVersion('1.2')
elif (self.provides or self.requires or self.obsoletes or
self.classifiers or self.download_url):
def assert_string_list(dist, attr, value):
- """Verify that value is a string list or None"""
+ """Verify that value is a string list"""
try:
+ # verify that value is a list or tuple to exclude unordered
+ # or single-use iterables
+ assert isinstance(value, (list, tuple))
+ # verify that elements of value are strings
assert ''.join(value) != value
except (TypeError, ValueError, AttributeError, AssertionError):
raise DistutilsSetupError(
def check_package_data(dist, attr, value):
"""Verify that value is a dictionary of package names to glob lists"""
- if isinstance(value, dict):
- for k, v in value.items():
- if not isinstance(k, str):
- break
- try:
- iter(v)
- except TypeError:
- break
- else:
- return
- raise DistutilsSetupError(
- attr + " must be a dictionary mapping package names to lists of "
- "wildcard patterns"
- )
+ if not isinstance(value, dict):
+ raise DistutilsSetupError(
+ "{!r} must be a dictionary mapping package names to lists of "
+ "string wildcard patterns".format(attr))
+ for k, v in value.items():
+ if not isinstance(k, six.string_types):
+ raise DistutilsSetupError(
+ "keys of {!r} dict must be strings (got {!r})"
+ .format(attr, k)
+ )
+ assert_string_list(dist, 'values of {!r} dict'.format(attr), v)
def check_packages(dist, attr, value):
Bytecode = getattr(dis, 'Bytecode', Bytecode_compat)
-unescape = getattr(html, 'unescape', html_parser.HTMLParser().unescape)
+unescape = getattr(html, 'unescape', None)
+if unescape is None:
+ # HTMLParser.unescape is deprecated since Python 3.4, and will be removed
+ # from 3.9.
+ unescape = html_parser.HTMLParser().unescape
assert os.path.isfile(os.path.join(dist_dir, wheel_name))
- def test_build_wheel_with_existing_wheel_file_present(self, tmpdir_cwd):
- # Building a wheel should still succeed if there's already a wheel
- # in the wheel directory
+ @pytest.mark.parametrize('build_type', ('wheel', 'sdist'))
+ def test_build_with_existing_file_present(self, build_type, tmpdir_cwd):
+ # Building a sdist/wheel should still succeed if there's
+ # already a sdist/wheel in the destination directory.
files = {
'setup.py': "from setuptools import setup\nsetup()",
'VERSION': "0.0.1",
build_files(files)
- dist_dir = os.path.abspath('pip-wheel-preexisting')
- os.makedirs(dist_dir)
+ dist_dir = os.path.abspath('preexisting-' + build_type)
- # make first wheel
build_backend = self.get_build_backend()
- wheel_one = build_backend.build_wheel(dist_dir)
+ build_method = getattr(build_backend, 'build_' + build_type)
+
+ # Build a first sdist/wheel.
+ # Note: this also check the destination directory is
+ # successfully created if it does not exist already.
+ first_result = build_method(dist_dir)
- # change version
+ # Change version.
with open("VERSION", "wt") as version_file:
version_file.write("0.0.2")
- # make *second* wheel
- wheel_two = self.get_build_backend().build_wheel(dist_dir)
+ # Build a *second* sdist/wheel.
+ second_result = build_method(dist_dir)
- assert os.path.isfile(os.path.join(dist_dir, wheel_one))
- assert wheel_one != wheel_two
+ assert os.path.isfile(os.path.join(dist_dir, first_result))
+ assert first_result != second_result
- # and if rebuilding the same wheel?
- open(os.path.join(dist_dir, wheel_two), 'w').close()
- wheel_three = self.get_build_backend().build_wheel(dist_dir)
- assert wheel_three == wheel_two
- assert os.path.getsize(os.path.join(dist_dir, wheel_three)) > 0
+ # And if rebuilding the exact same sdist/wheel?
+ open(os.path.join(dist_dir, second_result), 'w').close()
+ third_result = build_method(dist_dir)
+ assert third_result == second_result
+ assert os.path.getsize(os.path.join(dist_dir, third_result)) > 0
def test_build_sdist(self, build_backend):
dist_dir = os.path.abspath('pip-sdist')
from __future__ import unicode_literals
import io
-from setuptools.dist import DistDeprecationWarning, _get_unpatched
+import re
+from distutils.errors import DistutilsSetupError
+from setuptools.dist import (
+ _get_unpatched,
+ check_package_data,
+ DistDeprecationWarning,
+)
from setuptools import Distribution
from setuptools.extern.six.moves.urllib.request import pathname2url
from setuptools.extern.six.moves.urllib_parse import urljoin
else:
line = '%s: %s' % (fkey, val)
assert line in pkg_lines_set
+
+
+CHECK_PACKAGE_DATA_TESTS = (
+ # Valid.
+ ({
+ '': ['*.txt', '*.rst'],
+ 'hello': ['*.msg'],
+ }, None),
+ # Not a dictionary.
+ ((
+ ('', ['*.txt', '*.rst']),
+ ('hello', ['*.msg']),
+ ), (
+ "'package_data' must be a dictionary mapping package"
+ " names to lists of string wildcard patterns"
+ )),
+ # Invalid key type.
+ ({
+ 400: ['*.txt', '*.rst'],
+ }, (
+ "keys of 'package_data' dict must be strings (got 400)"
+ )),
+ # Invalid value type.
+ ({
+ 'hello': str('*.msg'),
+ }, (
+ "\"values of 'package_data' dict\" must be a list of strings (got '*.msg')"
+ )),
+ # Invalid value type (generators are single use)
+ ({
+ 'hello': (x for x in "generator"),
+ }, (
+ "\"values of 'package_data' dict\" must be a list of strings "
+ "(got <generator object"
+ )),
+)
+
+
+@pytest.mark.parametrize('package_data, expected_message', CHECK_PACKAGE_DATA_TESTS)
+def test_check_package_data(package_data, expected_message):
+ if expected_message is None:
+ assert check_package_data(None, 'package_data', package_data) is None
+ else:
+ with pytest.raises(DistutilsSetupError, match=re.escape(expected_message)):
+ check_package_data(None, str('package_data'), package_data)
assert expected_line in pkg_info_lines
expected_line = 'Project-URL: Link Two, https://example.com/two/'
assert expected_line in pkg_info_lines
+ assert 'Metadata-Version: 1.2' in pkg_info_lines
def test_python_requires_egg_info(self, tmpdir_cwd, env):
self._setup_script_with_requires(
allowed_unknowns = [
'test_suite',
'tests_require',
+ 'python_requires',
'install_requires',
]
assert not match or match.group(1).strip('"\'') in allowed_unknowns
def install(pkg_dir, install_dir):
with open(os.path.join(pkg_dir, 'setuptools.py'), 'w') as breaker:
breaker.write('raise ImportError()')
- cmd = [sys.executable, 'setup.py', 'install', '--prefix', install_dir]
- env = dict(os.environ, PYTHONPATH=pkg_dir)
+ cmd = [sys.executable, 'setup.py', 'install', '--prefix', str(install_dir)]
+ env = dict(os.environ, PYTHONPATH=str(pkg_dir))
output = subprocess.check_output(
cmd, cwd=pkg_dir, env=env, stderr=subprocess.STDOUT)
return output.decode('utf-8')
import pytest_virtualenv
+from setuptools.extern import six
+
from .textwrap import DALS
from .test_easy_install import make_nspkg_sdist
'pip==10.0.1',
'pip==18.1',
'pip==19.0.1',
- 'https://github.com/pypa/pip/archive/master.zip',
]
+ # Pip's master dropped support for 3.4.
+ if not six.PY34:
+ network_versions.append('https://github.com/pypa/pip/archive/master.zip')
+
versions = [None] + [
pytest.param(v, **({} if network else {'marks': pytest.mark.skip}))
for v in network_versions
pytest-flake8<=1.0.0; python_version=="3.4"
virtualenv>=13.0.0
pytest-virtualenv>=1.2.7
-# pytest pinned to <4 due to #1638
-pytest>=3.7,<4
+pytest>=3.7
wheel
coverage>=4.5.1
pytest-cov>=2.5.1
# from being added to `sys.path`.
install_command=python -c 'import sys; sys.path.remove(""); from pkg_resources import load_entry_point; load_entry_point("pip", "console_scripts", "pip")()' install {opts} {packages}
# Same as above.
-list_dependencies_command={envbindir}/pip freeze
+list_dependencies_command={envbindir}/pip freeze --all
setenv=COVERAGE_FILE={toxworkdir}/.coverage.{envname}
# TODO: The passed environment variables came from copying other tox.ini files
# These should probably be individually annotated to explain what needs them.