[bumpversion]
-current_version = 46.0.0
+current_version = 46.1.0
commit = True
tag = True
[bumpversion:file:setup.cfg]
-
+v46.1.0
+-------
+
+* #308: Allow version number normalization to be bypassed by wrapping in a 'setuptools.sic()' call.
+* #1424: Prevent keeping files mode for package_data build. It may break a build if user's package data has read only flag.
+* #1431: In ``easy_install.check_site_dir``, ensure the installation directory exists.
+* #1563: In ``pkg_resources`` prefer ``find_spec`` (PEP 451) to ``find_module``.
+
+Incorporate changes from v44.1.0:
+
+* #1704: Set sys.argv[0] in setup script run by build_meta.__legacy__
+* #1959: Fix for Python 4: replace unsafe six.PY3 with six.PY2
+* #1994: Fixed a bug in the "setuptools.finalize_distribution_options" hook that lead to ignoring the order attribute of entry points managed by this hook.
+
+
+v44.1.0
+-------
+
+* #1704: Set sys.argv[0] in setup script run by build_meta.__legacy__
+* #1959: Fix for Python 4: replace unsafe six.PY3 with six.PY2
+* #1994: Fixed a bug in the "setuptools.finalize_distribution_options" hook that lead to ignoring the order attribute of entry points managed by this hook.
+
+
v46.0.0
-------
===============
In order to allow for rapid, predictable releases, Setuptools uses a
-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 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.
-4. Verify that there are no remaining ``changelog.d/*.rst`` files. If a
- file was named incorrectly, it may be ignored by towncrier.
-5. Review the updated ``CHANGES.rst`` file. If any changes are needed,
- make the edits and stage them via ``git add CHANGES.rst``.
-
-Once the changelog edits are staged and ready to commit, cut a release by
-installing and running ``bump2version --allow-dirty {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::
-
- $ 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/
+mechanical technique for releases, enacted on tagged commits by
+continuous integration.
+
+To finalize a release, run ``tox -e finalize``, review, then push
+the changes.
+
+If tests pass, the release will be uploaded to PyPI.
Release Frequency
-----------------
if importer is None:
return None
- # capture warnings due to #1111
- with warnings.catch_warnings():
- warnings.simplefilter("ignore")
- loader = importer.find_module(packageName)
+ # use find_spec (PEP 451) and fall-back to find_module (PEP 302)
+ try:
+ loader = importer.find_spec(packageName).loader
+ except AttributeError:
+ # capture warnings due to #1111
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore")
+ loader = importer.find_module(packageName)
if loader is None:
return None
# Suppress other Python 2 UnicodeWarnings
ignore:Unicode equal comparison failed to convert:UnicodeWarning
ignore:Unicode unequal comparison failed to convert:UnicodeWarning
+ # https://github.com/pypa/setuptools/issues/2025
+ ignore:direct construction of .*Item has been deprecated:DeprecationWarning
[metadata]
name = setuptools
-version = 46.0.0
+version = 46.1.0
description = Easily download, build, install, upgrade, and uninstall Python packages
author = Python Packaging Authority
author_email = distutils-sig@python.org
return list(files)
+class sic(str):
+ """Treat this string as-is (https://en.wikipedia.org/wiki/Sic)"""
+
+
# Apply monkey patches
monkey.patch_all()
target = os.path.join(build_dir, filename)
self.mkpath(os.path.dirname(target))
srcfile = os.path.join(src_dir, filename)
- outf, copied = self.copy_file(srcfile, target)
+ outf, copied = self.copy_file(srcfile, target, preserve_mode=False)
srcfile = os.path.abspath(srcfile)
if (copied and
srcfile in self.distribution.convert_2to3_doctests):
instdir = normalize_path(self.install_dir)
pth_file = os.path.join(instdir, 'easy-install.pth')
+ if not os.path.exists(instdir):
+ try:
+ os.makedirs(instdir)
+ except (OSError, IOError):
+ self.cant_write_to_target()
+
# Is it a configured, PYTHONPATH, implicit, or explicit site dir?
is_site_dir = instdir in self.all_site_dirs
from . import SetuptoolsDeprecationWarning
+import setuptools
from setuptools import windows_support
from setuptools.monkey import get_unpatched
from setuptools.config import parse_configuration
value = default() if default else None
setattr(self.metadata, option, value)
- if isinstance(self.metadata.version, numbers.Number):
+ self.metadata.version = self._normalize_version(
+ self._validate_version(self.metadata.version))
+ self._finalize_requires()
+
+ @staticmethod
+ def _normalize_version(version):
+ if isinstance(version, setuptools.sic) or version is None:
+ return version
+
+ normalized = str(packaging.version.Version(version))
+ if version != normalized:
+ tmpl = "Normalizing '{version}' to '{normalized}'"
+ warnings.warn(tmpl.format(**locals()))
+ return normalized
+ return version
+
+ @staticmethod
+ def _validate_version(version):
+ if isinstance(version, numbers.Number):
# Some people apparently take "version number" too literally :)
- self.metadata.version = str(self.metadata.version)
+ version = str(version)
- if self.metadata.version is not None:
+ if version is not None:
try:
- ver = packaging.version.Version(self.metadata.version)
- normalized_version = str(ver)
- if self.metadata.version != normalized_version:
- warnings.warn(
- "Normalizing '%s' to '%s'" % (
- self.metadata.version,
- normalized_version,
- )
- )
- self.metadata.version = normalized_version
+ packaging.version.Version(version)
except (packaging.version.InvalidVersion, TypeError):
warnings.warn(
"The version specified (%r) is an invalid version, this "
"may not work as expected with newer versions of "
"setuptools, pip, and PyPI. Please see PEP 440 for more "
- "details." % self.metadata.version
+ "details." % version
)
- self._finalize_requires()
+ return setuptools.sic(version)
+ return version
def _finalize_requires(self):
"""
to influence the order of execution. Smaller numbers
go first and the default is 0.
"""
- hook_key = 'setuptools.finalize_distribution_options'
+ group = 'setuptools.finalize_distribution_options'
def by_order(hook):
return getattr(hook, 'order', 0)
- eps = pkg_resources.iter_entry_points(hook_key)
+ eps = map(lambda e: e.load(), pkg_resources.iter_entry_points(group))
for ep in sorted(eps, key=by_order):
- ep.load()(self)
+ ep(self)
def _finalize_setup_keywords(self):
for ep in pkg_resources.iter_entry_points('distutils.setup_keywords'):
import os
+import stat
+import shutil
from setuptools.dist import Distribution
os.makedirs('path/subpath')
dist.parse_command_line()
dist.run_commands()
+
+
+def test_read_only(tmpdir_cwd):
+ """
+ Ensure mode is not preserved in copy for package modules
+ and package data, as that causes problems
+ with deleting read-only files on Windows.
+
+ #1451
+ """
+ dist = Distribution(dict(
+ script_name='setup.py',
+ script_args=['build_py'],
+ packages=['pkg'],
+ package_data={'pkg': ['data.dat']},
+ name='pkg',
+ ))
+ os.makedirs('pkg')
+ open('pkg/__init__.py', 'w').close()
+ open('pkg/data.dat', 'w').close()
+ os.chmod('pkg/__init__.py', stat.S_IREAD)
+ os.chmod('pkg/data.dat', stat.S_IREAD)
+ dist.parse_command_line()
+ dist.run_commands()
+ shutil.rmtree('build')
import io
import collections
import re
+import functools
from distutils.errors import DistutilsSetupError
from setuptools.dist import (
_get_unpatched,
check_package_data,
DistDeprecationWarning,
)
+from setuptools import sic
from setuptools import Distribution
from setuptools.extern.six.moves.urllib.request import pathname2url
from setuptools.extern.six.moves.urllib_parse import urljoin
def __read_test_cases():
- # Metadata version 1.0
- base_attrs = {
- "name": "package",
- "version": "0.0.1",
- "author": "Foo Bar",
- "author_email": "foo@bar.net",
- "long_description": "Long\ndescription",
- "description": "Short description",
- "keywords": ["one", "two"]
- }
-
- def merge_dicts(d1, d2):
- d1 = d1.copy()
- d1.update(d2)
-
- return d1
+ base = dict(
+ name="package",
+ version="0.0.1",
+ author="Foo Bar",
+ author_email="foo@bar.net",
+ long_description="Long\ndescription",
+ description="Short description",
+ keywords=["one", "two"],
+ )
+
+ params = functools.partial(dict, base)
test_cases = [
- ('Metadata version 1.0', base_attrs.copy()),
- ('Metadata version 1.1: Provides', merge_dicts(base_attrs, {
- 'provides': ['package']
- })),
- ('Metadata version 1.1: Obsoletes', merge_dicts(base_attrs, {
- 'obsoletes': ['foo']
- })),
- ('Metadata version 1.1: Classifiers', merge_dicts(base_attrs, {
- 'classifiers': [
+ ('Metadata version 1.0', params()),
+ ('Metadata version 1.1: Provides', params(
+ provides=['package'],
+ )),
+ ('Metadata version 1.1: Obsoletes', params(
+ obsoletes=['foo'],
+ )),
+ ('Metadata version 1.1: Classifiers', params(
+ classifiers=[
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.7',
'License :: OSI Approved :: MIT License',
- ]})),
- ('Metadata version 1.1: Download URL', merge_dicts(base_attrs, {
- 'download_url': 'https://example.com'
- })),
- ('Metadata Version 1.2: Requires-Python', merge_dicts(base_attrs, {
- 'python_requires': '>=3.7'
- })),
+ ],
+ )),
+ ('Metadata version 1.1: Download URL', params(
+ download_url='https://example.com',
+ )),
+ ('Metadata Version 1.2: Requires-Python', params(
+ python_requires='>=3.7',
+ )),
pytest.param(
'Metadata Version 1.2: Project-Url',
- merge_dicts(base_attrs, {
- 'project_urls': {
- 'Foo': 'https://example.bar'
- }
- }), marks=pytest.mark.xfail(
- reason="Issue #1578: project_urls not read"
- )),
- ('Metadata Version 2.1: Long Description Content Type',
- merge_dicts(base_attrs, {
- 'long_description_content_type': 'text/x-rst; charset=UTF-8'
- })),
+ params(project_urls=dict(Foo='https://example.bar')),
+ marks=pytest.mark.xfail(
+ reason="Issue #1578: project_urls not read",
+ ),
+ ),
+ ('Metadata Version 2.1: Long Description Content Type', params(
+ long_description_content_type='text/x-rst; charset=UTF-8',
+ )),
pytest.param(
'Metadata Version 2.1: Provides Extra',
- merge_dicts(base_attrs, {
- 'provides_extras': ['foo', 'bar']
- }), marks=pytest.mark.xfail(reason="provides_extras not read")),
- ('Missing author, missing author e-mail',
- {'name': 'foo', 'version': '1.0.0'}),
- ('Missing author',
- {'name': 'foo',
- 'version': '1.0.0',
- 'author_email': 'snorri@sturluson.name'}),
- ('Missing author e-mail',
- {'name': 'foo',
- 'version': '1.0.0',
- 'author': 'Snorri Sturluson'}),
- ('Missing author',
- {'name': 'foo',
- 'version': '1.0.0',
- 'author': 'Snorri Sturluson'}),
+ params(provides_extras=['foo', 'bar']),
+ marks=pytest.mark.xfail(reason="provides_extras not read"),
+ ),
+ ('Missing author', dict(
+ name='foo',
+ version='1.0.0',
+ author_email='snorri@sturluson.name',
+ )),
+ ('Missing author e-mail', dict(
+ name='foo',
+ version='1.0.0',
+ author='Snorri Sturluson',
+ )),
+ ('Missing author and e-mail', dict(
+ name='foo',
+ version='1.0.0',
+ )),
+ ('Bypass normalized version', dict(
+ name='foo',
+ version=sic('1.0.0a'),
+ )),
]
return test_cases
--- /dev/null
+"""
+Finalize the repo for a release. Invokes towncrier and bumpversion.
+"""
+
+__requires__ = ['bump2version', 'towncrier']
+
+
+import subprocess
+import pathlib
+import re
+import sys
+
+
+def release_kind():
+ """
+ Determine which release to make based on the files in the
+ changelog.
+ """
+ # use min here as 'major' < 'minor' < 'patch'
+ return min(
+ 'major' if 'breaking' in file.name else
+ 'minor' if 'change' in file.name else
+ 'patch'
+ for file in pathlib.Path('changelog.d').iterdir()
+ )
+
+
+bump_version_command = [
+ sys.executable,
+ '-m', 'bumpversion',
+ release_kind(),
+]
+
+
+def get_version():
+ cmd = bump_version_command + ['--dry-run', '--verbose']
+ out = subprocess.check_output(cmd, text=True)
+ return re.search('^new_version=(.*)', out, re.MULTILINE).group(1)
+
+
+def update_changelog():
+ cmd = [
+ sys.executable, '-m',
+ 'towncrier',
+ '--version', get_version(),
+ '--yes',
+ ]
+ subprocess.check_call(cmd)
+
+
+def bump_version():
+ cmd = bump_version_command + ['--allow-dirty']
+ subprocess.check_call(cmd)
+
+
+if __name__ == '__main__':
+ print("Cutting release at", get_version())
+ update_changelog()
+ bump_version()
omit=
*/_vendor/*
+[testenv:finalize]
+skip_install = True
+deps =
+ towncrier
+ bump2version
+commands =
+ python tools/finalize.py
+
[testenv:release]
skip_install = True
deps =