diff options
author | Daniel Holth <dholth@fastmail.fm> | 2016-09-06 19:21:17 -0400 |
---|---|---|
committer | Daniel Holth <dholth@fastmail.fm> | 2016-09-06 19:21:17 -0400 |
commit | 2ad4bf75f76d91e4407ce7175130790199020b60 (patch) | |
tree | 8f5449b438be0da911fcecb591d495fd6ec484cd | |
parent | b96985e04be87bc062129be3e7fc23ea932fd524 (diff) | |
parent | 0b0668bebdfc59ce5ebebf4518ba5d1ca53bade3 (diff) | |
download | wheel-2ad4bf75f76d91e4407ce7175130790199020b60.tar.gz |
merge
-rw-r--r-- | docs/index.rst | 18 | ||||
-rw-r--r-- | wheel/archive.py | 3 | ||||
-rw-r--r-- | wheel/bdist_wheel.py | 31 | ||||
-rwxr-xr-x | wheel/egg2wheel.py | 17 | ||||
-rw-r--r-- | wheel/install.py | 12 | ||||
-rw-r--r-- | wheel/metadata.py | 5 | ||||
-rw-r--r-- | wheel/pep425tags.py | 11 | ||||
-rw-r--r-- | wheel/signatures/keys.py | 2 | ||||
-rw-r--r-- | wheel/test/conftest.py | 45 | ||||
-rw-r--r-- | wheel/test/extension.dist/extension.c | 2 | ||||
-rw-r--r-- | wheel/test/extension.dist/setup.cfg | 2 | ||||
-rw-r--r-- | wheel/test/extension.dist/setup.py | 20 | ||||
-rw-r--r-- | wheel/test/test_basic.py | 16 | ||||
-rw-r--r-- | wheel/tool/__init__.py | 9 | ||||
-rwxr-xr-x | wheel/wininst2wheel.py | 33 |
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") |