summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.txt15
-rw-r--r--README.txt9
-rw-r--r--tox.ini2
-rw-r--r--wheel/__init__.py2
-rw-r--r--wheel/archive.py26
-rw-r--r--wheel/bdist_wheel.py49
-rw-r--r--wheel/metadata.py13
-rw-r--r--wheel/pep425tags.py97
-rw-r--r--wheel/test/complex-dist/setup.py7
-rw-r--r--wheel/test/test_tagopt.py69
-rw-r--r--wheel/test/test_wheelfile.py71
11 files changed, 308 insertions, 52 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index 26754c9..afee416 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,3 +1,16 @@
+0.26.0
+======
+- Fix multiple entrypoint comparison failure on Python 3 (Issue #148)
+
+0.25.0
+======
+- Add Python 3.5 to tox configuration
+- Deterministic (sorted) metadata
+- Fix tagging for Python 3.5 compatibility
+- Support py2-none-'arch' and py3-none-'arch' tags
+- Treat data-only wheels as pure
+- Write to temporary file and rename when using wheel install --force
+
0.24.0
======
- The python tag used for pure-python packages is now .pyN (major version
@@ -14,7 +27,7 @@
0.23.0
======
-- Compatibiltiy tag flags added to the bdist_wheel command
+- Compatibility tag flags added to the bdist_wheel command
- sdist should include files necessary for tests
- 'wheel convert' can now also convert unpacked eggs to wheel
- Rename pydist.json to metadata.json to avoid stepping on the PEP
diff --git a/README.txt b/README.txt
index 4b14821..7b37ad9 100644
--- a/README.txt
+++ b/README.txt
@@ -39,3 +39,12 @@ Unlike .egg, wheel will be a fully-documented standard at the binary
level that is truly easy to install even if you do not want to use the
reference implementation.
+
+Code of Conduct
+---------------
+
+Everyone interacting in the wheel project's codebases, issue trackers, chat
+rooms, and mailing lists is expected to follow the `PyPA Code of Conduct`_.
+
+.. _PyPA Code of Conduct: https://www.pypa.io/en/latest/code-of-conduct/
+
diff --git a/tox.ini b/tox.ini
index f1e6a10..8930062 100644
--- a/tox.ini
+++ b/tox.ini
@@ -4,7 +4,7 @@
# and then run "tox" from this directory.
[tox]
-envlist = py26, py27, pypy, py33, py34
+envlist = py26, py27, pypy, py33, py34, py35
[testenv]
commands =
diff --git a/wheel/__init__.py b/wheel/__init__.py
index b84d0e4..5a57ba7 100644
--- a/wheel/__init__.py
+++ b/wheel/__init__.py
@@ -1,2 +1,2 @@
# __variables__ with double-quoted values will be available in setup.py:
-__version__ = "0.25.0"
+__version__ = "0.26.0"
diff --git a/wheel/archive.py b/wheel/archive.py
index 225d295..f4dd617 100644
--- a/wheel/archive.py
+++ b/wheel/archive.py
@@ -2,6 +2,8 @@
Archive tools for wheel.
"""
+import os
+import time
import logging
import os.path
import zipfile
@@ -31,6 +33,15 @@ def make_wheelfile_inner(base_name, base_dir='.'):
log.info("creating '%s' and adding '%s' to it", zip_filename, base_dir)
+ # Some applications need reproducible .whl files, but they can't do this
+ # without forcing the timestamp of the individual ZipInfo objects. See
+ # issue #143.
+ timestamp = os.environ.get('SOURCE_DATE_EPOCH')
+ if timestamp is None:
+ date_time = None
+ else:
+ date_time = time.gmtime(int(timestamp))[0:6]
+
# XXX support bz2, xz when available
zip = zipfile.ZipFile(open(zip_filename, "wb+"), "w",
compression=zipfile.ZIP_DEFLATED)
@@ -38,8 +49,15 @@ def make_wheelfile_inner(base_name, base_dir='.'):
score = {'WHEEL': 1, 'METADATA': 2, 'RECORD': 3}
deferred = []
- def writefile(path):
- zip.write(path, path)
+ def writefile(path, date_time):
+ if date_time is None:
+ st = os.stat(path)
+ mtime = time.gmtime(st.st_mtime)
+ date_time = mtime[0:6]
+ zinfo = zipfile.ZipInfo(path, date_time)
+ zinfo.external_attr = 0o100644 << 16
+ with open(path, 'rb') as fp:
+ zip.writestr(zinfo, fp.read())
log.info("adding '%s'" % path)
for dirpath, dirnames, filenames in os.walk(base_dir):
@@ -50,11 +68,11 @@ def make_wheelfile_inner(base_name, base_dir='.'):
if dirpath.endswith('.dist-info'):
deferred.append((score.get(name, 0), path))
else:
- writefile(path)
+ writefile(path, date_time)
deferred.sort()
for score, path in deferred:
- writefile(path)
+ writefile(path, date_time)
zip.close()
diff --git a/wheel/bdist_wheel.py b/wheel/bdist_wheel.py
index ac770e9..8920fce 100644
--- a/wheel/bdist_wheel.py
+++ b/wheel/bdist_wheel.py
@@ -33,7 +33,7 @@ from distutils.sysconfig import get_python_version
from distutils import log as logger
-from .pep425tags import get_abbr_impl, get_impl_ver
+from .pep425tags import get_abbr_impl, get_impl_ver, get_abi_tag
from .util import native, open_for_csv
from .archive import archive_wheelfile
from .pkginfo import read_pkg_info, write_pkg_info
@@ -52,9 +52,12 @@ class bdist_wheel(Command):
user_options = [('bdist-dir=', 'b',
"temporary directory for creating the distribution"),
- ('plat-name=', 'p',
- "platform name to embed in generated filenames "
+ ('plat-tag=', 'p',
+ "platform tag to embed in generated filenames "
"(default: %s)" % get_platform()),
+ ('plat-name=', None,
+ "DEPRECATED. Platform tag to embed in generated "
+ "filenames (default: %s)" % get_platform()),
('keep-temp', 'k',
"keep the pseudo-installation tree around after " +
"creating the distribution archive"),
@@ -85,6 +88,7 @@ class bdist_wheel(Command):
self.bdist_dir = None
self.data_dir = None
self.plat_name = None
+ self.plat_tag = None
self.format = 'zip'
self.keep_temp = False
self.dist_dir = None
@@ -128,30 +132,27 @@ class bdist_wheel(Command):
safer_version(self.distribution.get_version())))
def get_tag(self):
- supported_tags = pep425tags.get_supported()
+ plat_tag = self.plat_tag or self.plat_name
+ if plat_tag:
+ plat_tag = plat_tag.replace('-', '_').replace('.', '_')
+ supported_tags = pep425tags.get_supported(supplied_platform=plat_tag)
if self.root_is_pure:
if self.universal:
impl = 'py2.py3'
else:
impl = self.python_tag
- tag = (impl, 'none', 'any')
+ if not plat_tag:
+ plat_tag = 'any'
+ tag = (impl, 'none', plat_tag)
else:
- plat_name = self.plat_name
- if plat_name is None:
- plat_name = get_platform()
- plat_name = plat_name.replace('-', '_').replace('.', '_')
impl_name = get_abbr_impl()
impl_ver = get_impl_ver()
- # PEP 3149 -- no SOABI in Py 2
- # For PyPy?
- # "pp%s%s" % (sys.pypy_version_info.major,
- # sys.pypy_version_info.minor)
- abi_tag = sysconfig.get_config_vars().get('SOABI', 'none')
- if abi_tag.startswith('cpython-'):
- abi_tag = 'cp' + abi_tag.rsplit('-', 1)[-1]
-
- tag = (impl_name + impl_ver, abi_tag, plat_name)
+ # PEP 3149
+ abi_tag = str(get_abi_tag()).lower()
+ if not plat_tag:
+ plat_tag = get_platform().replace('-', '_').replace('.', '_')
+ tag = (impl_name + impl_ver, abi_tag, plat_tag)
# XXX switch to this alternate implementation for non-pure:
assert tag == supported_tags[0]
return tag
@@ -200,7 +201,7 @@ class bdist_wheel(Command):
if os.name == 'nt':
# win32 barfs if any of these are ''; could be '.'?
# (distutils.command.install:change_roots bug)
- basedir_observed = os.path.join(self.data_dir, '..')
+ basedir_observed = os.path.normpath(os.path.join(self.data_dir, '..'))
self.install_libbase = self.install_lib = basedir_observed
setattr(install,
@@ -378,9 +379,11 @@ class bdist_wheel(Command):
'not-zip-safe',)))
# delete dependency_links if it is only whitespace
- dependency_links = os.path.join(distinfo_path, 'dependency_links.txt')
- if not open(dependency_links, 'r').read().strip():
- adios(dependency_links)
+ dependency_links_path = os.path.join(distinfo_path, 'dependency_links.txt')
+ with open(dependency_links_path, 'r') as dependency_links_file:
+ dependency_links = dependency_links_file.read().strip()
+ if not dependency_links:
+ adios(dependency_links_path)
write_pkg_info(os.path.join(distinfo_path, 'METADATA'), pkg_info)
@@ -410,7 +413,7 @@ class bdist_wheel(Command):
pymeta['extensions']['python.details']['document_names']['license'] = license_filename
with open(metadata_json_path, "w") as metadata_json:
- json.dump(pymeta, metadata_json)
+ json.dump(pymeta, metadata_json, sort_keys=True)
adios(egginfo_path)
diff --git a/wheel/metadata.py b/wheel/metadata.py
index 02c0663..b3cc65c 100644
--- a/wheel/metadata.py
+++ b/wheel/metadata.py
@@ -74,7 +74,14 @@ def handle_requires(metadata, pkg_info, key):
if may_requires:
metadata['run_requires'] = []
- for key, value in may_requires.items():
+ def sort_key(item):
+ # Both condition and extra could be None, which can't be compared
+ # against strings in Python 3.
+ key, value = item
+ if key.condition is None:
+ return ''
+ return key.condition
+ for key, value in sorted(may_requires.items(), key=sort_key):
may_requirement = OrderedDict((('requires', value),))
if key.extra:
may_requirement['extra'] = key.extra
@@ -191,8 +198,8 @@ def pkginfo_to_dict(path, distribution=None):
exports = OrderedDict()
for group, items in sorted(ep_map.items()):
exports[group] = OrderedDict()
- for item in sorted(items.values()):
- name, export = str(item).split(' = ', 1)
+ for item in sorted(map(str, items.values())):
+ name, export = item.split(' = ', 1)
exports[group][name] = export
if exports:
metadata['extensions']['python.exports'] = exports
diff --git a/wheel/pep425tags.py b/wheel/pep425tags.py
index a6220ea..106c879 100644
--- a/wheel/pep425tags.py
+++ b/wheel/pep425tags.py
@@ -1,6 +1,7 @@
"""Generate and work with PEP 425 Compatibility Tags."""
import sys
+import warnings
try:
import sysconfig
@@ -10,6 +11,14 @@ except ImportError: # pragma nocover
import distutils.util
+def get_config_var(var):
+ try:
+ return sysconfig.get_config_var(var)
+ except IOError as e: # pip Issue #1074
+ warnings.warn("{0}".format(e), RuntimeWarning)
+ return None
+
+
def get_abbr_impl():
"""Return abbreviated implementation name."""
if hasattr(sys, 'pypy_version_info'):
@@ -25,19 +34,76 @@ def get_abbr_impl():
def get_impl_ver():
"""Return implementation version."""
- impl_ver = sysconfig.get_config_var("py_version_nodot")
- if not impl_ver:
- impl_ver = ''.join(map(str, sys.version_info[:2]))
+ impl_ver = get_config_var("py_version_nodot")
+ if not impl_ver or get_abbr_impl() == 'pp':
+ impl_ver = ''.join(map(str, get_impl_version_info()))
return impl_ver
+def get_impl_version_info():
+ """Return sys.version_info-like tuple for use in decrementing the minor
+ version."""
+ if get_abbr_impl() == 'pp':
+ # as per https://github.com/pypa/pip/issues/2882
+ return (sys.version_info[0], sys.pypy_version_info.major,
+ sys.pypy_version_info.minor)
+ else:
+ return sys.version_info[0], sys.version_info[1]
+
+
+def get_flag(var, fallback, expected=True, warn=True):
+ """Use a fallback method for determining SOABI flags if the needed config
+ var is unset or unavailable."""
+ val = get_config_var(var)
+ if val is None:
+ if warn:
+ warnings.warn("Config variable '{0}' is unset, Python ABI tag may "
+ "be incorrect".format(var), RuntimeWarning, 2)
+ return fallback()
+ return val == expected
+
+
+def get_abi_tag():
+ """Return the ABI tag based on SOABI (if available) or emulate SOABI
+ (CPython 2, PyPy)."""
+ soabi = get_config_var('SOABI')
+ impl = get_abbr_impl()
+ if not soabi and impl in ('cp', 'pp') and hasattr(sys, 'maxunicode'):
+ d = ''
+ m = ''
+ u = ''
+ if get_flag('Py_DEBUG',
+ lambda: hasattr(sys, 'gettotalrefcount'),
+ warn=(impl == 'cp')):
+ d = 'd'
+ if get_flag('WITH_PYMALLOC',
+ lambda: impl == 'cp',
+ warn=(impl == 'cp')):
+ m = 'm'
+ if get_flag('Py_UNICODE_SIZE',
+ lambda: sys.maxunicode == 0x10ffff,
+ expected=4,
+ warn=(impl == 'cp' and
+ sys.version_info < (3, 3))) \
+ and sys.version_info < (3, 3):
+ u = 'u'
+ abi = '%s%s%s%s%s' % (impl, get_impl_ver(), d, m, u)
+ elif soabi and soabi.startswith('cpython-'):
+ abi = 'cp' + soabi.split('-')[1]
+ elif soabi:
+ abi = soabi.replace('.', '_').replace('-', '_')
+ else:
+ abi = None
+ return abi
+
+
def get_platform():
"""Return our platform name 'win32', 'linux_x86_64'"""
# XXX remove distutils dependency
return distutils.util.get_platform().replace('.', '_').replace('-', '_')
-def get_supported(versions=None):
+def get_supported(versions=None, supplied_platform=None):
"""Return a list of supported tags for each version specified in
`versions`.
@@ -49,18 +115,19 @@ def get_supported(versions=None):
# Versions must be given with respect to the preference
if versions is None:
versions = []
- major = sys.version_info[0]
+ version_info = get_impl_version_info()
+ major = version_info[:-1]
# Support all previous minor Python versions.
- for minor in range(sys.version_info[1], -1, -1):
- versions.append(''.join(map(str, (major, minor))))
+ for minor in range(version_info[-1], -1, -1):
+ versions.append(''.join(map(str, major + (minor,))))
impl = get_abbr_impl()
abis = []
- soabi = sysconfig.get_config_var('SOABI')
- if soabi and soabi.startswith('cpython-'):
- abis[0:0] = ['cp' + soabi.split('-', 1)[-1]]
+ abi = get_abi_tag()
+ if abi:
+ abis[0:0] = [abi]
abi3s = set()
import imp
@@ -72,11 +139,15 @@ def get_supported(versions=None):
abis.append('none')
- arch = get_platform()
+ platforms = []
+ if supplied_platform:
+ platforms.append(supplied_platform)
+ platforms.append(get_platform())
# Current version, current API (built specifically for our Python):
for abi in abis:
- supported.append(('%s%s' % (impl, versions[0]), abi, arch))
+ for arch in platforms:
+ supported.append(('%s%s' % (impl, versions[0]), abi, arch))
# No abi / arch, but requires our implementation:
for i, version in enumerate(versions):
@@ -96,5 +167,3 @@ def get_supported(versions=None):
supported.append(('py%s' % (version[0]), 'none', 'any'))
return supported
-
-
diff --git a/wheel/test/complex-dist/setup.py b/wheel/test/complex-dist/setup.py
index 802dbaf..615d5dc 100644
--- a/wheel/test/complex-dist/setup.py
+++ b/wheel/test/complex-dist/setup.py
@@ -20,6 +20,11 @@ setup(name='complex-dist',
install_requires=["quux", "splort"],
extras_require={'simple':['simple.dist']},
tests_require=["foo", "bar>=10.0.0"],
- entry_points={'console_scripts':['complex-dist=complexdist:main']}
+ entry_points={
+ 'console_scripts': [
+ 'complex-dist=complexdist:main',
+ 'complex-dist2=complexdist:main',
+ ],
+ },
)
diff --git a/wheel/test/test_tagopt.py b/wheel/test/test_tagopt.py
index 300fcf9..a8de089 100644
--- a/wheel/test/test_tagopt.py
+++ b/wheel/test/test_tagopt.py
@@ -10,27 +10,39 @@ import tempfile
import subprocess
SETUP_PY = """\
-from setuptools import setup
+from setuptools import setup, Extension
setup(
name="Test",
version="1.0",
author_email="author@example.com",
py_modules=["test"],
+ {ext_modules}
)
"""
+EXT_MODULES = "ext_modules=[Extension('_test', sources=['test.c'])],"
+
@pytest.fixture
-def temp_pkg(request):
+def temp_pkg(request, ext=False):
tempdir = tempfile.mkdtemp()
def fin():
shutil.rmtree(tempdir)
request.addfinalizer(fin)
temppath = py.path.local(tempdir)
temppath.join('test.py').write('print("Hello, world")')
- temppath.join('setup.py').write(SETUP_PY)
+ if ext:
+ temppath.join('test.c').write('#include <stdio.h>')
+ setup_py = SETUP_PY.format(ext_modules=EXT_MODULES)
+ else:
+ setup_py = SETUP_PY.format(ext_modules='')
+ temppath.join('setup.py').write(setup_py)
return temppath
+@pytest.fixture
+def temp_ext_pkg(request):
+ return temp_pkg(request, ext=True)
+
def test_default_tag(temp_pkg):
subprocess.check_call([sys.executable, 'setup.py', 'bdist_wheel'],
cwd=str(temp_pkg))
@@ -110,3 +122,54 @@ def test_legacy_wheel_section_in_setup_cfg(temp_pkg):
assert wheels[0].basename.startswith('Test-1.0-py2.py3-')
assert wheels[0].ext == '.whl'
+def test_plat_tag_purepy(temp_pkg):
+ subprocess.check_call(
+ [sys.executable, 'setup.py', 'bdist_wheel', '--plat-tag=testplat.pure'],
+ cwd=str(temp_pkg))
+ dist_dir = temp_pkg.join('dist')
+ assert dist_dir.check(dir=1)
+ wheels = dist_dir.listdir()
+ assert len(wheels) == 1
+ assert wheels[0].basename.endswith('-testplat_pure.whl')
+ assert wheels[0].ext == '.whl'
+
+def test_plat_tag_ext(temp_ext_pkg):
+ try:
+ subprocess.check_call(
+ [sys.executable, 'setup.py', 'bdist_wheel', '--plat-tag=testplat.arch'],
+ cwd=str(temp_ext_pkg))
+ except subprocess.CalledProcessError:
+ pytest.skip("Cannot compile C Extensions")
+ dist_dir = temp_ext_pkg.join('dist')
+ assert dist_dir.check(dir=1)
+ wheels = dist_dir.listdir()
+ assert len(wheels) == 1
+ assert wheels[0].basename.endswith('-testplat_arch.whl')
+ assert wheels[0].ext == '.whl'
+
+def test_plat_tag_purepy_in_setupcfg(temp_pkg):
+ temp_pkg.join('setup.cfg').write('[bdist_wheel]\nplat_tag=testplat.pure')
+ subprocess.check_call(
+ [sys.executable, 'setup.py', 'bdist_wheel'],
+ cwd=str(temp_pkg))
+ dist_dir = temp_pkg.join('dist')
+ assert dist_dir.check(dir=1)
+ wheels = dist_dir.listdir()
+ assert len(wheels) == 1
+ assert wheels[0].basename.endswith('-testplat_pure.whl')
+ assert wheels[0].ext == '.whl'
+
+def test_plat_tag_ext_in_setupcfg(temp_ext_pkg):
+ temp_ext_pkg.join('setup.cfg').write('[bdist_wheel]\nplat_tag=testplat.arch')
+ try:
+ subprocess.check_call(
+ [sys.executable, 'setup.py', 'bdist_wheel'],
+ cwd=str(temp_ext_pkg))
+ except subprocess.CalledProcessError:
+ pytest.skip("Cannot compile C Extensions")
+ dist_dir = temp_ext_pkg.join('dist')
+ assert dist_dir.check(dir=1)
+ wheels = dist_dir.listdir()
+ assert len(wheels) == 1
+ assert wheels[0].basename.endswith('-testplat_arch.whl')
+ assert wheels[0].ext == '.whl'
diff --git a/wheel/test/test_wheelfile.py b/wheel/test/test_wheelfile.py
index e362ceb..59bbb4c 100644
--- a/wheel/test/test_wheelfile.py
+++ b/wheel/test/test_wheelfile.py
@@ -1,11 +1,48 @@
+import os
import wheel.install
+import wheel.archive
import hashlib
try:
from StringIO import StringIO
except ImportError:
from io import BytesIO as StringIO
+import codecs
import zipfile
import pytest
+import shutil
+import tempfile
+from contextlib import contextmanager
+
+@contextmanager
+def environ(key, value):
+ old_value = os.environ.get(key)
+ try:
+ os.environ[key] = value
+ yield
+ finally:
+ if old_value is None:
+ del os.environ[key]
+ else:
+ os.environ[key] = old_value
+
+@contextmanager
+def temporary_directory():
+ # tempfile.TemporaryDirectory doesn't exist in Python 2.
+ tempdir = tempfile.mkdtemp()
+ try:
+ yield tempdir
+ finally:
+ shutil.rmtree(tempdir)
+
+@contextmanager
+def readable_zipfile(path):
+ # zipfile.ZipFile() isn't a context manager under Python 2.
+ zf = zipfile.ZipFile(path, 'r')
+ try:
+ yield zf
+ finally:
+ zf.close()
+
def test_verifying_zipfile():
if not hasattr(zipfile.ZipExtFile, '_update_crc'):
@@ -66,4 +103,36 @@ def test_pop_zipfile():
zf = wheel.install.VerifyingZipFile(sio, 'r')
assert len(zf.infolist()) == 1
- \ No newline at end of file
+
+def test_zipfile_timestamp():
+ # An environment variable can be used to influence the timestamp on
+ # TarInfo objects inside the zip. See issue #143. TemporaryDirectory is
+ # not a context manager under Python 3.
+ with temporary_directory() as tempdir:
+ for filename in ('one', 'two', 'three'):
+ path = os.path.join(tempdir, filename)
+ with codecs.open(path, 'w', encoding='utf-8') as fp:
+ fp.write(filename + '\n')
+ zip_base_name = os.path.join(tempdir, 'dummy')
+ # The earliest date representable in TarInfos, 1980-01-01
+ with environ('SOURCE_DATE_EPOCH', '315576060'):
+ zip_filename = wheel.archive.make_wheelfile_inner(
+ zip_base_name, tempdir)
+ with readable_zipfile(zip_filename) as zf:
+ for info in zf.infolist():
+ assert info.date_time[:3] == (1980, 1, 1)
+
+def test_zipfile_attributes():
+ # With the change from ZipFile.write() to .writestr(), we need to manually
+ # set member attributes. Per existing tradition file permissions are forced
+ # to 0o644, although in the future we may want to preserve executable bits.
+ with temporary_directory() as tempdir:
+ path = os.path.join(tempdir, 'foo')
+ with codecs.open(path, 'w', encoding='utf-8') as fp:
+ fp.write('foo\n')
+ zip_base_name = os.path.join(tempdir, 'dummy')
+ zip_filename = wheel.archive.make_wheelfile_inner(
+ zip_base_name, tempdir)
+ with readable_zipfile(zip_filename) as zf:
+ for info in zf.infolist():
+ assert info.external_attr == 0o100644 << 16