summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Holth <dholth@fastmail.fm>2016-09-06 19:21:17 -0400
committerDaniel Holth <dholth@fastmail.fm>2016-09-06 19:21:17 -0400
commit2ad4bf75f76d91e4407ce7175130790199020b60 (patch)
tree8f5449b438be0da911fcecb591d495fd6ec484cd
parentb96985e04be87bc062129be3e7fc23ea932fd524 (diff)
parent0b0668bebdfc59ce5ebebf4518ba5d1ca53bade3 (diff)
downloadwheel-2ad4bf75f76d91e4407ce7175130790199020b60.tar.gz
merge
-rw-r--r--docs/index.rst18
-rw-r--r--wheel/archive.py3
-rw-r--r--wheel/bdist_wheel.py31
-rwxr-xr-xwheel/egg2wheel.py17
-rw-r--r--wheel/install.py12
-rw-r--r--wheel/metadata.py5
-rw-r--r--wheel/pep425tags.py11
-rw-r--r--wheel/signatures/keys.py2
-rw-r--r--wheel/test/conftest.py45
-rw-r--r--wheel/test/extension.dist/extension.c2
-rw-r--r--wheel/test/extension.dist/setup.cfg2
-rw-r--r--wheel/test/extension.dist/setup.py20
-rw-r--r--wheel/test/test_basic.py16
-rw-r--r--wheel/tool/__init__.py9
-rwxr-xr-xwheel/wininst2wheel.py33
15 files changed, 188 insertions, 38 deletions
diff --git a/docs/index.rst b/docs/index.rst
index 5b1b157..f1dbce6 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -111,9 +111,21 @@ used to specify the Python version tag to use more precisely::
equates to the tag "py2.py3".
--python-tag XXX Specifies the precise python version tag to use for
a pure-python wheel.
-
-Neither of these two flags have any effect when used on a project that includes
-C extension code.
+ --py-limited-api {cp32|cp33|cp34|...}
+ Specifies Python Py_LIMITED_API compatibility with
+ the version of CPython passed and later versions.
+ The wheel will be tagged cpNN.abi3.{arch} on CPython 3.
+ This flag does not affect Python 2 builds or alternate
+ Python implementations.
+
+ To conform to the limited API, all your C
+ extensions must use only functions from the limited
+ API, pass Extension(py_limited_api=True) and e.g.
+ #define Py_LIMITED_API=0x03020000 depending on
+ the exact minimun Python you wish to support.
+
+The --universal and --python-tag flags have no effect when used on a
+project that includes C extension code.
The default for a pure Python project (if no explicit flags are given) is "pyN"
where N is the major version of the Python interpreter used to build the wheel.
diff --git a/wheel/archive.py b/wheel/archive.py
index f928e6a..fa30a70 100644
--- a/wheel/archive.py
+++ b/wheel/archive.py
@@ -43,8 +43,7 @@ def make_wheelfile_inner(base_name, base_dir='.'):
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)
+ zip = zipfile.ZipFile(zip_filename, "w", compression=zipfile.ZIP_DEFLATED)
score = {'WHEEL': 1, 'METADATA': 2, 'RECORD': 3}
deferred = []
diff --git a/wheel/bdist_wheel.py b/wheel/bdist_wheel.py
index 3dc2caf..edbba05 100644
--- a/wheel/bdist_wheel.py
+++ b/wheel/bdist_wheel.py
@@ -12,7 +12,7 @@ import warnings
import shutil
import json
import sys
-import wheel
+import re
try:
import sysconfig
@@ -39,6 +39,9 @@ from .archive import archive_wheelfile
from .pkginfo import read_pkg_info, write_pkg_info
from .metadata import pkginfo_to_dict
from . import pep425tags, metadata
+from . import __version__ as wheel_version
+
+PY_LIMITED_API_PATTERN = r'cp3\d'
def safer_name(name):
return safe_name(name).replace('-', '_')
@@ -77,6 +80,9 @@ class bdist_wheel(Command):
('python-tag=', None,
"Python implementation compatibility tag"
" (default: py%s)" % get_impl_ver()[0]),
+ ('py-limited-api=', None,
+ "Python tag (cp32|cp33|cpNN) for abi3 wheel tag"
+ " (default: false)"),
]
boolean_options = ['keep-temp', 'skip-build', 'relative', 'universal']
@@ -98,6 +104,7 @@ class bdist_wheel(Command):
self.group = None
self.universal = False
self.python_tag = 'py' + get_impl_ver()[0]
+ self.py_limited_api = False
self.plat_name_supplied = False
def finalize_options(self):
@@ -116,6 +123,9 @@ class bdist_wheel(Command):
self.root_is_pure = not (self.distribution.has_ext_modules()
or self.distribution.has_c_libraries())
+ if self.py_limited_api and not re.match(PY_LIMITED_API_PATTERN, self.py_limited_api):
+ raise ValueError("py-limited-api must match '%s'" % PY_LIMITED_API_PATTERN)
+
# Support legacy [wheel] section for setting universal
wheel = self.distribution.get_option_dict('wheel')
if 'universal' in wheel:
@@ -153,13 +163,20 @@ class bdist_wheel(Command):
else:
impl_name = get_abbr_impl()
impl_ver = get_impl_ver()
- # PEP 3149
- abi_tag = str(get_abi_tag()).lower()
- tag = (impl_name + impl_ver, abi_tag, plat_name)
+ impl = impl_name + impl_ver
+ # We don't work on CPython 3.1, 3.0.
+ if self.py_limited_api and (impl_name + impl_ver).startswith('cp3'):
+ impl = self.py_limited_api
+ abi_tag = 'abi3'
+ else:
+ abi_tag = str(get_abi_tag()).lower()
+ tag = (impl, abi_tag, plat_name)
supported_tags = pep425tags.get_supported(
supplied_platform=plat_name if self.plat_name_supplied else None)
# XXX switch to this alternate implementation for non-pure:
- assert tag == supported_tags[0], "%s != %s" % (tag, supported_tags[0])
+ if not self.py_limited_api:
+ assert tag == supported_tags[0], "%s != %s" % (tag, supported_tags[0])
+ assert tag in supported_tags, "would build wheel with unsupported tag %s" % tag
return tag
def get_archive_basename(self):
@@ -257,7 +274,7 @@ class bdist_wheel(Command):
else:
rmtree(self.bdist_dir)
- def write_wheelfile(self, wheelfile_base, generator='bdist_wheel (' + wheel.__version__ + ')'):
+ def write_wheelfile(self, wheelfile_base, generator='bdist_wheel (' + wheel_version + ')'):
from email.message import Message
msg = Message()
msg['Wheel-Version'] = '1.0' # of the spec
@@ -423,7 +440,7 @@ class bdist_wheel(Command):
adios(egginfo_path)
def write_record(self, bdist_dir, distinfo_dir):
- from wheel.util import urlsafe_b64encode
+ from .util import urlsafe_b64encode
record_path = os.path.join(distinfo_dir, 'RECORD')
record_relpath = os.path.relpath(record_path, bdist_dir)
diff --git a/wheel/egg2wheel.py b/wheel/egg2wheel.py
index bf919c4..e8d3153 100755
--- a/wheel/egg2wheel.py
+++ b/wheel/egg2wheel.py
@@ -10,6 +10,7 @@ import distutils.dist
from distutils.archive_util import make_archive
from argparse import ArgumentParser
from glob import iglob
+from wheel.wininst2wheel import _bdist_wheel_tag
egg_info_re = re.compile(r'''(?P<name>.+?)-(?P<ver>.+?)
(-(?P<pyver>.+?))?(-(?P<arch>.+?))?.egg''', re.VERBOSE)
@@ -43,8 +44,20 @@ def egg2wheel(egg_path, dest_dir):
abi,
arch
))
- bw = wheel.bdist_wheel.bdist_wheel(distutils.dist.Distribution())
- bw.root_is_purelib = egg_info['arch'] is None
+ root_is_purelib = egg_info['arch'] is None
+ if root_is_purelib:
+ bw = wheel.bdist_wheel.bdist_wheel(distutils.dist.Distribution())
+ else:
+ bw = _bdist_wheel_tag(distutils.dist.Distribution())
+
+ bw.root_is_pure = root_is_purelib
+ bw.python_tag = pyver
+ bw.plat_name_supplied = True
+ bw.plat_name = egg_info['arch'] or 'any'
+ if not root_is_purelib:
+ bw.full_tag_supplied = True
+ bw.full_tag = (pyver, abi, arch)
+
dist_info_dir = os.path.join(dir, '%s.dist-info' % dist_info)
bw.egg2dist(os.path.join(dir, 'EGG-INFO'),
dist_info_dir)
diff --git a/wheel/install.py b/wheel/install.py
index 9d48efe..a422b0e 100644
--- a/wheel/install.py
+++ b/wheel/install.py
@@ -18,12 +18,12 @@ try:
except NameError:
_big_number = sys.maxint
-from wheel.decorator import reify
-from wheel.util import (urlsafe_b64encode, from_json, urlsafe_b64decode,
- native, binary, HashingFile)
-from wheel import signatures
-from wheel.pkginfo import read_pkg_info_bytes
-from wheel.util import open_for_csv
+from .decorator import reify
+from .util import (urlsafe_b64encode, from_json, urlsafe_b64decode,
+ native, binary, HashingFile)
+from . import signatures
+from .pkginfo import read_pkg_info_bytes
+from .util import open_for_csv
from .pep425tags import get_supported
from .paths import get_install_paths
diff --git a/wheel/metadata.py b/wheel/metadata.py
index 6710677..341c4b0 100644
--- a/wheel/metadata.py
+++ b/wheel/metadata.py
@@ -15,7 +15,8 @@ import os.path
import textwrap
import pkg_resources
import email.parser
-import wheel
+
+from . import __version__ as wheel_version
METADATA_VERSION = "2.0"
@@ -106,7 +107,7 @@ def pkginfo_to_dict(path, distribution=None):
"""
metadata = OrderedDefaultDict(lambda: OrderedDefaultDict(lambda: OrderedDefaultDict(OrderedDict)))
- metadata["generator"] = "bdist_wheel (" + wheel.__version__ + ")"
+ metadata["generator"] = "bdist_wheel (" + wheel_version + ")"
try:
unicode
pkg_info = read_pkg_info(path)
diff --git a/wheel/pep425tags.py b/wheel/pep425tags.py
index 5ac5d0d..49a1367 100644
--- a/wheel/pep425tags.py
+++ b/wheel/pep425tags.py
@@ -152,7 +152,16 @@ def get_supported(versions=None, supplied_platform=None):
for abi in abis:
for arch in platforms:
supported.append(('%s%s' % (impl, versions[0]), abi, arch))
-
+
+ # abi3 modules compatible with older version of Python
+ for version in versions[1:]:
+ # abi3 was introduced in Python 3.2
+ if version in ('31', '30'):
+ break
+ for abi in abi3s: # empty set if not Python 3
+ for arch in platforms:
+ supported.append(("%s%s" % (impl, version), abi, arch))
+
# No abi / arch, but requires our implementation:
for i, version in enumerate(versions):
supported.append(('%s%s' % (impl, version), 'none', 'any'))
diff --git a/wheel/signatures/keys.py b/wheel/signatures/keys.py
index 1dde4bf..57d7feb 100644
--- a/wheel/signatures/keys.py
+++ b/wheel/signatures/keys.py
@@ -33,7 +33,7 @@ wheel export key
import json
import os.path
-from wheel.util import native, load_config_paths, save_config_path
+from ..util import native, load_config_paths, save_config_path
class WheelKeys(object):
SCHEMA = 1
diff --git a/wheel/test/conftest.py b/wheel/test/conftest.py
new file mode 100644
index 0000000..d14cc47
--- /dev/null
+++ b/wheel/test/conftest.py
@@ -0,0 +1,45 @@
+"""
+pytest local configuration plug-in
+"""
+
+import gc
+import warnings
+
+import pytest
+
+@pytest.yield_fixture(scope='function', autouse=True)
+def error_on_ResourceWarning():
+ """This fixture captures ResourceWarning's and reports an "error"
+ describing the file handles left open.
+
+ This is shown regardless of how successful the test was, if a test fails
+ and leaves files open then those files will be reported. Ideally, even
+ those files should be closed properly after a test failure or exception.
+
+ Since only Python 3 and PyPy3 have ResourceWarning's, this context will
+ have no effect when running tests on Python 2 or PyPy.
+
+ Because of autouse=True, this function will be automatically enabled for
+ all test_* functions in this module.
+
+ This code is primarily based on the examples found here:
+ https://stackoverflow.com/questions/24717027/convert-python-3-resourcewarnings-into-exception
+ """
+ try:
+ ResourceWarning
+ except NameError:
+ # Python 2, PyPy
+ yield
+ return
+ # Python 3, PyPy3
+ with warnings.catch_warnings(record=True) as caught:
+ warnings.resetwarnings() # clear all filters
+ warnings.simplefilter('ignore') # ignore all
+ warnings.simplefilter('always', ResourceWarning) # add filter
+ yield # run tests in this context
+ gc.collect() # run garbage collection (for pypy3)
+ if not caught:
+ return
+ pytest.fail('The following file descriptors were not closed properly:\n' +
+ '\n'.join((str(warning.message) for warning in caught)),
+ pytrace=False)
diff --git a/wheel/test/extension.dist/extension.c b/wheel/test/extension.dist/extension.c
new file mode 100644
index 0000000..a37c3fa
--- /dev/null
+++ b/wheel/test/extension.dist/extension.c
@@ -0,0 +1,2 @@
+#define Py_LIMITED_API 0x03020000
+#include <Python.h>
diff --git a/wheel/test/extension.dist/setup.cfg b/wheel/test/extension.dist/setup.cfg
new file mode 100644
index 0000000..9f6ff39
--- /dev/null
+++ b/wheel/test/extension.dist/setup.cfg
@@ -0,0 +1,2 @@
+[bdist_wheel]
+py_limited_api=cp32
diff --git a/wheel/test/extension.dist/setup.py b/wheel/test/extension.dist/setup.py
new file mode 100644
index 0000000..7a66845
--- /dev/null
+++ b/wheel/test/extension.dist/setup.py
@@ -0,0 +1,20 @@
+from setuptools import setup, Extension
+
+try:
+ unicode
+ def u8(s):
+ return s.decode('unicode-escape').encode('utf-8')
+except NameError:
+ def u8(s):
+ return s.encode('utf-8')
+
+setup(name='extension.dist',
+ version='0.1',
+ description=u8('A testing distribution \N{SNOWMAN}'),
+ ext_modules=[
+ Extension(name='extension',
+ sources=['extension.c'],
+ py_limited_api=True)
+ ],
+ )
+
diff --git a/wheel/test/test_basic.py b/wheel/test/test_basic.py
index e69fef9..6bd46b1 100644
--- a/wheel/test/test_basic.py
+++ b/wheel/test/test_basic.py
@@ -63,12 +63,13 @@ def test_findable():
def test_egg_re():
"""Make sure egg_info_re matches."""
- egg_names = open(pkg_resources.resource_filename('wheel', 'eggnames.txt'))
- for line in egg_names:
- line = line.strip()
- if not line:
- continue
- assert egg2wheel.egg_info_re.match(line), line
+ egg_names_path = pkg_resources.resource_filename('wheel', 'eggnames.txt')
+ with open(egg_names_path) as egg_names:
+ for line in egg_names:
+ line = line.strip()
+ if not line:
+ continue
+ assert egg2wheel.egg_info_re.match(line), line
def test_compatibility_tags():
"""Test compatibilty tags are working."""
@@ -117,7 +118,8 @@ def test_pydist():
import jsonschema
def open_json(filename):
- return json.loads(open(filename, 'rb').read().decode('utf-8'))
+ with open(filename, 'rb') as json_file:
+ return json.loads(json_file.read().decode('utf-8'))
pymeta_schema = open_json(resource_filename('wheel.test',
'pydist-schema.json'))
diff --git a/wheel/tool/__init__.py b/wheel/tool/__init__.py
index 95f0a9b..4c0187b 100644
--- a/wheel/tool/__init__.py
+++ b/wheel/tool/__init__.py
@@ -6,13 +6,13 @@ import os
import hashlib
import sys
import json
-import wheel.paths
from glob import iglob
from .. import signatures
from ..util import (urlsafe_b64decode, urlsafe_b64encode, native, binary,
matches_requirement)
-from ..install import WheelFile
+from ..install import WheelFile, VerifyingZipFile
+from ..paths import get_install_command
def require_pkgresources(name):
try:
@@ -97,8 +97,7 @@ def unsign(wheelfile):
ordinary archive, with the compressed files and the directory in the same
order, and without any non-zip content after the truncation point.
"""
- import wheel.install
- vzf = wheel.install.VerifyingZipFile(wheelfile, "a")
+ vzf = VerifyingZipFile(wheelfile, "a")
info = vzf.infolist()
if not (len(info) and info[-1].filename.endswith('/RECORD.jws')):
raise WheelError("RECORD.jws not found at end of archive.")
@@ -233,7 +232,7 @@ def install_scripts(distributions):
for dist in distributions:
pkg_resources_dist = pkg_resources.get_distribution(dist)
- install = wheel.paths.get_install_command(dist)
+ install = get_install_command(dist)
command = easy_install.easy_install(install.distribution)
command.args = ['wheel'] # dummy argument
command.finalize_options()
diff --git a/wheel/wininst2wheel.py b/wheel/wininst2wheel.py
index 297f8d1..15f0cdf 100755
--- a/wheel/wininst2wheel.py
+++ b/wheel/wininst2wheel.py
@@ -158,8 +158,20 @@ def bdist_wininst2wheel(path, dest_dir=os.path.curdir):
abi,
arch
))
- bw = wheel.bdist_wheel.bdist_wheel(distutils.dist.Distribution())
- bw.root_is_purelib = root_is_purelib
+ if root_is_purelib:
+ bw = wheel.bdist_wheel.bdist_wheel(distutils.dist.Distribution())
+ else:
+ bw = _bdist_wheel_tag(distutils.dist.Distribution())
+
+ bw.root_is_pure = root_is_purelib
+ bw.python_tag = pyver
+ bw.plat_name_supplied = True
+ bw.plat_name = info['arch'] or 'any'
+
+ if not root_is_purelib:
+ bw.full_tag_supplied = True
+ bw.full_tag = (pyver, abi, arch)
+
dist_info_dir = os.path.join(dir, '%s.dist-info' % dist_info)
bw.egg2dist(os.path.join(dir, egginfo_name), dist_info_dir)
bw.write_wheelfile(dist_info_dir, generator='wininst2wheel')
@@ -168,6 +180,23 @@ def bdist_wininst2wheel(path, dest_dir=os.path.curdir):
archive_wheelfile(os.path.join(dest_dir, wheel_name), dir)
rmtree(dir)
+
+class _bdist_wheel_tag(wheel.bdist_wheel.bdist_wheel):
+ # allow the client to override the default generated wheel tag
+ # The default bdist_wheel implementation uses python and abi tags
+ # of the running python process. This is not suitable for
+ # generating/repackaging prebuild binaries.
+
+ full_tag_supplied = False
+ full_tag = None # None or a (pytag, soabitag, plattag) triple
+
+ def get_tag(self):
+ if self.full_tag_supplied and self.full_tag is not None:
+ return self.full_tag
+ else:
+ return super(_bdist_wheel_tag, self).get_tag()
+
+
def main():
parser = ArgumentParser()
parser.add_argument('installers', nargs='*', help="Installers to convert")