Commit c0edf932 authored by Jason R. Coombs's avatar Jason R. Coombs Committed by GitHub

Merge branch 'master' into master

parents 7af7f8a6 85747b8c
v38.2.6
-------
* #1207: Add support for ``long_description_type`` to setup.cfg
declarative config as intended and documented.
v38.2.5
-------
* #1232: Fix trailing slash handling in ``pkg_resources.ZipProvider``.
v38.2.4
-------
* #1220: Fix `data_files` handling when installing from wheel.
v38.2.3
-------
* fix Travis' Python 3.3 job.
v38.2.2
-------
* #1214: fix handling of namespace packages when installing
from a wheel.
v38.2.1
-------
* #1212: fix encoding handling of metadata when installing
from a wheel.
v38.2.0
-------
* #1200: easy_install now support installing from wheels:
they will be installed as standalone unzipped eggs.
v38.1.0
-------
* #1208: Improve error message when failing to locate scripts
in egg-info metadata.
v38.0.0
-------
* #458: In order to support deterministic builds, Setuptools no
longer allows packages to declare ``install_requires`` as
unordered sequences (sets or dicts).
v37.0.0 v37.0.0
------- -------
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
:target: https://setuptools.readthedocs.io :target: https://setuptools.readthedocs.io
.. image:: https://img.shields.io/travis/pypa/setuptools/master.svg?label=Linux%20build%20%40%20Travis%20CI .. image:: https://img.shields.io/travis/pypa/setuptools/master.svg?label=Linux%20build%20%40%20Travis%20CI
:target: http://travis-ci.org/pypa/setuptools :target: https://travis-ci.org/pypa/setuptools
.. image:: https://img.shields.io/appveyor/ci/jaraco/setuptools/master.svg?label=Windows%20build%20%40%20Appveyor .. image:: https://img.shields.io/appveyor/ci/jaraco/setuptools/master.svg?label=Windows%20build%20%40%20Appveyor
:target: https://ci.appveyor.com/project/jaraco/setuptools/branch/master :target: https://ci.appveyor.com/project/jaraco/setuptools/branch/master
......
...@@ -1513,7 +1513,10 @@ class NullProvider: ...@@ -1513,7 +1513,10 @@ class NullProvider:
def run_script(self, script_name, namespace): def run_script(self, script_name, namespace):
script = 'scripts/' + script_name script = 'scripts/' + script_name
if not self.has_metadata(script): if not self.has_metadata(script):
raise ResolutionError("No script named %r" % script_name) raise ResolutionError(
"Script {script!r} not found in metadata at {self.egg_info!r}"
.format(**locals()),
)
script_text = self.get_metadata(script).replace('\r\n', '\n') script_text = self.get_metadata(script).replace('\r\n', '\n')
script_text = script_text.replace('\r', '\n') script_text = script_text.replace('\r', '\n')
script_filename = self._fn(self.egg_info, script) script_filename = self._fn(self.egg_info, script)
...@@ -1690,6 +1693,9 @@ class ZipProvider(EggProvider): ...@@ -1690,6 +1693,9 @@ class ZipProvider(EggProvider):
def _zipinfo_name(self, fspath): def _zipinfo_name(self, fspath):
# Convert a virtual filename (full path to file) into a zipfile subpath # Convert a virtual filename (full path to file) into a zipfile subpath
# usable with the zipimport directory cache for our target archive # usable with the zipimport directory cache for our target archive
fspath = fspath.rstrip(os.sep)
if fspath == self.loader.archive:
return ''
if fspath.startswith(self.zip_pre): if fspath.startswith(self.zip_pre):
return fspath[len(self.zip_pre):] return fspath[len(self.zip_pre):]
raise AssertionError( raise AssertionError(
......
...@@ -62,10 +62,21 @@ class TestZipProvider(object): ...@@ -62,10 +62,21 @@ class TestZipProvider(object):
zip_info.filename = 'data.dat' zip_info.filename = 'data.dat'
zip_info.date_time = cls.ref_time.timetuple() zip_info.date_time = cls.ref_time.timetuple()
zip_egg.writestr(zip_info, 'hello, world!') zip_egg.writestr(zip_info, 'hello, world!')
zip_info = zipfile.ZipInfo()
zip_info.filename = 'subdir/mod2.py'
zip_info.date_time = cls.ref_time.timetuple()
zip_egg.writestr(zip_info, 'x = 6\n')
zip_info = zipfile.ZipInfo()
zip_info.filename = 'subdir/data2.dat'
zip_info.date_time = cls.ref_time.timetuple()
zip_egg.writestr(zip_info, 'goodbye, world!')
zip_egg.close() zip_egg.close()
egg.close() egg.close()
sys.path.append(egg.name) sys.path.append(egg.name)
subdir = os.path.join(egg.name, 'subdir')
sys.path.append(subdir)
cls.finalizers.append(EggRemover(subdir))
cls.finalizers.append(EggRemover(egg.name)) cls.finalizers.append(EggRemover(egg.name))
@classmethod @classmethod
...@@ -73,6 +84,30 @@ class TestZipProvider(object): ...@@ -73,6 +84,30 @@ class TestZipProvider(object):
for finalizer in cls.finalizers: for finalizer in cls.finalizers:
finalizer() finalizer()
def test_resource_listdir(self):
import mod
zp = pkg_resources.ZipProvider(mod)
expected_root = ['data.dat', 'mod.py', 'subdir']
assert sorted(zp.resource_listdir('')) == expected_root
assert sorted(zp.resource_listdir('/')) == expected_root
expected_subdir = ['data2.dat', 'mod2.py']
assert sorted(zp.resource_listdir('subdir')) == expected_subdir
assert sorted(zp.resource_listdir('subdir/')) == expected_subdir
assert zp.resource_listdir('nonexistent') == []
assert zp.resource_listdir('nonexistent/') == []
import mod2
zp2 = pkg_resources.ZipProvider(mod2)
assert sorted(zp2.resource_listdir('')) == expected_subdir
assert sorted(zp2.resource_listdir('/')) == expected_subdir
assert zp2.resource_listdir('subdir') == []
assert zp2.resource_listdir('subdir/') == []
def test_resource_filename_rewrites_on_change(self): def test_resource_filename_rewrites_on_change(self):
""" """
If a previous call to get_resource_filename has saved the file, but If a previous call to get_resource_filename has saved the file, but
......
[bumpversion] [bumpversion]
current_version = 37.0.0 current_version = 38.2.5
commit = True commit = True
tag = True tag = True
......
...@@ -89,7 +89,7 @@ def pypi_link(pkg_filename): ...@@ -89,7 +89,7 @@ def pypi_link(pkg_filename):
setup_params = dict( setup_params = dict(
name="setuptools", name="setuptools",
version="37.0.0", version="38.2.5",
description="Easily download, build, install, upgrade, and uninstall " description="Easily download, build, install, upgrade, and uninstall "
"Python packages", "Python packages",
author="Python Packaging Authority", author="Python Packaging Authority",
......
...@@ -53,6 +53,7 @@ from setuptools.package_index import ( ...@@ -53,6 +53,7 @@ from setuptools.package_index import (
PackageIndex, parse_requirement_arg, URL_SCHEME, PackageIndex, parse_requirement_arg, URL_SCHEME,
) )
from setuptools.command import bdist_egg, egg_info from setuptools.command import bdist_egg, egg_info
from setuptools.wheel import Wheel
from pkg_resources import ( from pkg_resources import (
yield_lines, normalize_path, resource_string, ensure_directory, yield_lines, normalize_path, resource_string, ensure_directory,
get_distribution, find_distributions, Environment, Requirement, get_distribution, find_distributions, Environment, Requirement,
...@@ -842,6 +843,8 @@ class easy_install(Command): ...@@ -842,6 +843,8 @@ class easy_install(Command):
return [self.install_egg(dist_filename, tmpdir)] return [self.install_egg(dist_filename, tmpdir)]
elif dist_filename.lower().endswith('.exe'): elif dist_filename.lower().endswith('.exe'):
return [self.install_exe(dist_filename, tmpdir)] return [self.install_exe(dist_filename, tmpdir)]
elif dist_filename.lower().endswith('.whl'):
return [self.install_wheel(dist_filename, tmpdir)]
# Anything else, try to extract and build # Anything else, try to extract and build
setup_base = tmpdir setup_base = tmpdir
...@@ -1038,6 +1041,35 @@ class easy_install(Command): ...@@ -1038,6 +1041,35 @@ class easy_install(Command):
f.write('\n'.join(locals()[name]) + '\n') f.write('\n'.join(locals()[name]) + '\n')
f.close() f.close()
def install_wheel(self, wheel_path, tmpdir):
wheel = Wheel(wheel_path)
assert wheel.is_compatible()
destination = os.path.join(self.install_dir, wheel.egg_name())
destination = os.path.abspath(destination)
if not self.dry_run:
ensure_directory(destination)
if os.path.isdir(destination) and not os.path.islink(destination):
dir_util.remove_tree(destination, dry_run=self.dry_run)
elif os.path.exists(destination):
self.execute(
os.unlink,
(destination,),
"Removing " + destination,
)
try:
self.execute(
wheel.install_as_egg,
(destination,),
("Installing %s to %s") % (
os.path.basename(wheel_path),
os.path.dirname(destination)
),
)
finally:
update_dist_caches(destination, fix_zipimporter_caches=False)
self.add_output(destination)
return self.egg_distribution(destination)
__mv_warning = textwrap.dedent(""" __mv_warning = textwrap.dedent("""
Because this distribution was installed --multi-version, before you can Because this distribution was installed --multi-version, before you can
import modules from this package in an application, you will need to import modules from this package in an application, you will need to
......
...@@ -597,10 +597,7 @@ def write_pkg_info(cmd, basename, filename): ...@@ -597,10 +597,7 @@ def write_pkg_info(cmd, basename, filename):
metadata = cmd.distribution.metadata metadata = cmd.distribution.metadata
metadata.version, oldver = cmd.egg_version, metadata.version metadata.version, oldver = cmd.egg_version, metadata.version
metadata.name, oldname = cmd.egg_name, metadata.name metadata.name, oldname = cmd.egg_name, metadata.name
metadata.long_description_content_type = getattr(
cmd.distribution,
'long_description_content_type'
)
try: try:
# write unescaped data to PKG-INFO, so older pkg_resources # write unescaped data to PKG-INFO, so older pkg_resources
# can still parse it # can still parse it
...@@ -640,7 +637,7 @@ def write_requirements(cmd, basename, filename): ...@@ -640,7 +637,7 @@ def write_requirements(cmd, basename, filename):
def write_setup_requirements(cmd, basename, filename): def write_setup_requirements(cmd, basename, filename):
data = StringIO() data = io.StringIO()
_write_requirements(data, cmd.distribution.setup_requires) _write_requirements(data, cmd.distribution.setup_requires)
cmd.write_or_delete_file("setup-requirements", filename, data.getvalue()) cmd.write_or_delete_file("setup-requirements", filename, data.getvalue())
......
...@@ -60,11 +60,8 @@ def write_pkg_file(self, file): ...@@ -60,11 +60,8 @@ def write_pkg_file(self, file):
for project_url in self.project_urls.items(): for project_url in self.project_urls.items():
file.write('Project-URL: %s, %s\n' % project_url) file.write('Project-URL: %s, %s\n' % project_url)
long_desc_content_type = getattr( long_desc_content_type = \
self, self.long_description_content_type or 'UNKNOWN'
'long_description_content_type',
None
) or 'UNKNOWN'
file.write('Description-Content-Type: %s\n' % long_desc_content_type) file.write('Description-Content-Type: %s\n' % long_desc_content_type)
long_desc = rfc822_escape(self.get_long_description()) long_desc = rfc822_escape(self.get_long_description())
...@@ -168,6 +165,8 @@ def check_requirements(dist, attr, value): ...@@ -168,6 +165,8 @@ def check_requirements(dist, attr, value):
"""Verify that install_requires is a valid requirements list""" """Verify that install_requires is a valid requirements list"""
try: try:
list(pkg_resources.parse_requirements(value)) list(pkg_resources.parse_requirements(value))
if isinstance(value, (dict, set)):
raise TypeError("Unordered types are not allowed")
except (TypeError, ValueError) as error: except (TypeError, ValueError) as error:
tmpl = ( tmpl = (
"{attr!r} must be a string or list of strings " "{attr!r} must be a string or list of strings "
...@@ -340,6 +339,9 @@ class Distribution(Distribution_parse_config_files, _Distribution): ...@@ -340,6 +339,9 @@ class Distribution(Distribution_parse_config_files, _Distribution):
# prime it here from our value if not automatically set # prime it here from our value if not automatically set
self.metadata.project_urls = getattr( self.metadata.project_urls = getattr(
self.metadata, 'project_urls', self.project_urls) self.metadata, 'project_urls', self.project_urls)
self.metadata.long_description_content_type = attrs.get(
'long_description_content_type'
)
if isinstance(self.metadata.version, numbers.Number): if isinstance(self.metadata.version, numbers.Number):
# Some people apparently take "version number" too literally :) # Some people apparently take "version number" too literally :)
......
# This file originally from pip:
# https://github.com/pypa/pip/blob/8f4f15a5a95d7d5b511ceaee9ed261176c181970/src/pip/_internal/utils/glibc.py
from __future__ import absolute_import
import ctypes
import re
import warnings
def glibc_version_string():
"Returns glibc version string, or None if not using glibc."
# ctypes.CDLL(None) internally calls dlopen(NULL), and as the dlopen
# manpage says, "If filename is NULL, then the returned handle is for the
# main program". This way we can let the linker do the work to figure out
# which libc our process is actually using.
process_namespace = ctypes.CDLL(None)
try:
gnu_get_libc_version = process_namespace.gnu_get_libc_version
except AttributeError:
# Symbol doesn't exist -> therefore, we are not linked to
# glibc.
return None
# Call gnu_get_libc_version, which returns a string like "2.5"
gnu_get_libc_version.restype = ctypes.c_char_p
version_str = gnu_get_libc_version()
# py2 / py3 compatibility:
if not isinstance(version_str, str):
version_str = version_str.decode("ascii")
return version_str
# Separated out from have_compatible_glibc for easier unit testing
def check_glibc_version(version_str, required_major, minimum_minor):
# Parse string and check against requested version.
#
# We use a regexp instead of str.split because we want to discard any
# random junk that might come after the minor version -- this might happen
# in patched/forked versions of glibc (e.g. Linaro's version of glibc
# uses version strings like "2.20-2014.11"). See gh-3588.
m = re.match(r"(?P<major>[0-9]+)\.(?P<minor>[0-9]+)", version_str)
if not m:
warnings.warn("Expected glibc version with 2 components major.minor,"
" got: %s" % version_str, RuntimeWarning)
return False
return (int(m.group("major")) == required_major and
int(m.group("minor")) >= minimum_minor)
def have_compatible_glibc(required_major, minimum_minor):
version_str = glibc_version_string()
if version_str is None:
return False
return check_glibc_version(version_str, required_major, minimum_minor)
# platform.libc_ver regularly returns completely nonsensical glibc
# versions. E.g. on my computer, platform says:
#
# ~$ python2.7 -c 'import platform; print(platform.libc_ver())'
# ('glibc', '2.7')
# ~$ python3.5 -c 'import platform; print(platform.libc_ver())'
# ('glibc', '2.9')
#
# But the truth is:
#
# ~$ ldd --version
# ldd (Debian GLIBC 2.22-11) 2.22
#
# This is unfortunate, because it means that the linehaul data on libc
# versions that was generated by pip 8.1.2 and earlier is useless and
# misleading. Solution: instead of using platform, use our code that actually
# works.
def libc_ver():
"""Try to determine the glibc version
Returns a tuple of strings (lib, version) which default to empty strings
in case the lookup fails.
"""
glibc_version = glibc_version_string()
if glibc_version is None:
return ("", "")
else:
return ("glibc", glibc_version)
...@@ -21,13 +21,14 @@ import setuptools ...@@ -21,13 +21,14 @@ import setuptools
from pkg_resources import ( from pkg_resources import (
CHECKOUT_DIST, Distribution, BINARY_DIST, normalize_path, SOURCE_DIST, CHECKOUT_DIST, Distribution, BINARY_DIST, normalize_path, SOURCE_DIST,
Environment, find_distributions, safe_name, safe_version, Environment, find_distributions, safe_name, safe_version,
to_filename, Requirement, DEVELOP_DIST, to_filename, Requirement, DEVELOP_DIST, EGG_DIST,
) )
from setuptools import ssl_support from setuptools import ssl_support
from distutils import log from distutils import log
from distutils.errors import DistutilsError from distutils.errors import DistutilsError
from fnmatch import translate from fnmatch import translate
from setuptools.py27compat import get_all_headers from setuptools.py27compat import get_all_headers
from setuptools.wheel import Wheel
EGG_FRAGMENT = re.compile(r'^egg=([-A-Za-z0-9_.+!]+)$') EGG_FRAGMENT = re.compile(r'^egg=([-A-Za-z0-9_.+!]+)$')
HREF = re.compile("""href\\s*=\\s*['"]?([^'"> ]+)""", re.I) HREF = re.compile("""href\\s*=\\s*['"]?([^'"> ]+)""", re.I)
...@@ -115,6 +116,17 @@ def distros_for_location(location, basename, metadata=None): ...@@ -115,6 +116,17 @@ def distros_for_location(location, basename, metadata=None):
if basename.endswith('.egg') and '-' in basename: if basename.endswith('.egg') and '-' in basename:
# only one, unambiguous interpretation # only one, unambiguous interpretation
return [Distribution.from_location(location, basename, metadata)] return [Distribution.from_location(location, basename, metadata)]
if basename.endswith('.whl') and '-' in basename:
wheel = Wheel(basename)
if not wheel.is_compatible():
return []
return [Distribution(
location=location,
project_name=wheel.project_name,
version=wheel.version,
# Increase priority over eggs.
precedence=EGG_DIST + 1,
)]
if basename.endswith('.exe'): if basename.endswith('.exe'):
win_base, py_ver, platform = parse_bdist_wininst(basename) win_base, py_ver, platform = parse_bdist_wininst(basename)
if win_base is not None: if win_base is not None:
......
# This file originally from pip:
# https://github.com/pypa/pip/blob/8f4f15a5a95d7d5b511ceaee9ed261176c181970/src/pip/_internal/pep425tags.py
"""Generate and work with PEP 425 Compatibility Tags."""
from __future__ import absolute_import
import distutils.util
import platform
import re
import sys
import sysconfig
import warnings
from collections import OrderedDict
from . import glibc
_osx_arch_pat = re.compile(r'(.+)_(\d+)_(\d+)_(.+)')
def get_config_var(var):
try:
return sysconfig.get_config_var(var)
except IOError as e: # Issue #1074
warnings.warn("{}".format(e), RuntimeWarning)
return None
def get_abbr_impl():
"""Return abbreviated implementation name."""
if hasattr(sys, 'pypy_version_info'):
pyimpl = 'pp'
elif sys.platform.startswith('java'):
pyimpl = 'jy'
elif sys.platform == 'cli':
pyimpl = 'ip'
else:
pyimpl = 'cp'
return pyimpl
def get_impl_ver():
"""Return implementation version."""
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_impl_tag():
"""
Returns the Tag for this specific implementation.
"""
return "{}{}".format(get_abbr_impl(), get_impl_ver())
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 _is_running_32bit():
return sys.maxsize == 2147483647
def get_platform():
"""Return our platform name 'win32', 'linux_x86_64'"""
if sys.platform == 'darwin':
# distutils.util.get_platform() returns the release based on the value
# of MACOSX_DEPLOYMENT_TARGET on which Python was built, which may
# be significantly older than the user's current machine.
release, _, machine = platform.mac_ver()
split_ver = release.split('.')
if machine == "x86_64" and _is_running_32bit():
machine = "i386"
elif machine == "ppc64" and _is_running_32bit():
machine = "ppc"
return 'macosx_{}_{}_{}'.format(split_ver[0], split_ver[1], machine)
# XXX remove distutils dependency
result = distutils.util.get_platform().replace('.', '_').replace('-', '_')
if result == "linux_x86_64" and _is_running_32bit():
# 32 bit Python program (running on a 64 bit Linux): pip should only
# install and run 32 bit compiled extensions in that case.
result = "linux_i686"
return result
def is_manylinux1_compatible():
# Only Linux, and only x86-64 / i686
if get_platform() not in {"linux_x86_64", "linux_i686"}:
return False
# Check for presence of _manylinux module
try:
import _manylinux
return bool(_manylinux.manylinux1_compatible)
except (ImportError, AttributeError):
# Fall through to heuristic check below
pass
# Check glibc version. CentOS 5 uses glibc 2.5.
return glibc.have_compatible_glibc(2, 5)
def get_darwin_arches(major, minor, machine):
"""Return a list of supported arches (including group arches) for
the given major, minor and machine architecture of an macOS machine.
"""
arches = []
def _supports_arch(major, minor, arch):
# Looking at the application support for macOS versions in the chart
# provided by https://en.wikipedia.org/wiki/OS_X#Versions it appears
# our timeline looks roughly like:
#
# 10.0 - Introduces ppc support.
# 10.4 - Introduces ppc64, i386, and x86_64 support, however the ppc64
# and x86_64 support is CLI only, and cannot be used for GUI
# applications.
# 10.5 - Extends ppc64 and x86_64 support to cover GUI applications.
# 10.6 - Drops support for ppc64
# 10.7 - Drops support for ppc
#
# Given that we do not know if we're installing a CLI or a GUI
# application, we must be conservative and assume it might be a GUI
# application and behave as if ppc64 and x86_64 support did not occur
# until 10.5.
#
# Note: The above information is taken from the "Application support"
# column in the chart not the "Processor support" since I believe
# that we care about what instruction sets an application can use
# not which processors the OS supports.
if arch == 'ppc':
return (major, minor) <= (10, 5)
if arch == 'ppc64':
return (major, minor) == (10, 5)
if arch == 'i386':
return (major, minor) >= (10, 4)
if arch == 'x86_64':
return (major, minor) >= (10, 5)
if arch in groups:
for garch in groups[arch]:
if _supports_arch(major, minor, garch):
return True
return False
groups = OrderedDict([
("fat", ("i386", "ppc")),
("intel", ("x86_64", "i386")),
("fat64", ("x86_64", "ppc64")),
("fat32", ("x86_64", "i386", "ppc")),
])
if _supports_arch(major, minor, machine):
arches.append(machine)
for garch in groups:
if machine in groups[garch] and _supports_arch(major, minor, garch):
arches.append(garch)
arches.append('universal')
return arches
def get_supported(versions=None, noarch=False, platform=None,
impl=None, abi=None):
"""Return a list of supported tags for each version specified in
`versions`.
:param versions: a list of string versions, of the form ["33", "32"],
or None. The first version will be assumed to support our ABI.
:param platform: specify the exact platform you want valid
tags for, or None. If None, use the local system platform.
:param impl: specify the exact implementation you want valid
tags for, or None. If None, use the local interpreter impl.
:param abi: specify the exact abi you want valid
tags for, or None. If None, use the local interpreter abi.
"""
supported = []
# Versions must be given with respect to the preference
if versions is None:
versions = []
version_info = get_impl_version_info()
major = version_info[:-1]
# Support all previous minor Python versions.
for minor in range(version_info[-1], -1, -1):
versions.append(''.join(map(str, major + (minor,))))
impl = impl or get_abbr_impl()
abis = []
abi = abi or get_abi_tag()
if abi:
abis[0:0] = [abi]
abi3s = set()
import imp
for suffix in imp.get_suffixes():
if suffix[0].startswith('.abi'):
abi3s.add(suffix[0].split('.', 2)[1])
abis.extend(sorted(list(abi3s)))
abis.append('none')
if not noarch:
arch = platform or get_platform()
if arch.startswith('macosx'):
# support macosx-10.6-intel on macosx-10.9-x86_64
match = _osx_arch_pat.match(arch)
if match:
name, major, minor, actual_arch = match.groups()
tpl = '{}_{}_%i_%s'.format(name, major)
arches = []
for m in reversed(range(int(minor) + 1)):
for a in get_darwin_arches(int(major), m, actual_arch):
arches.append(tpl % (m, a))
else:
# arch pattern didn't match (?!)
arches = [arch]
elif platform is None and is_manylinux1_compatible():
arches = [arch.replace('linux', 'manylinux1'), arch]
else:
arches = [arch]
# Current version, current API (built specifically for our Python):
for abi in abis:
for arch in arches:
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 arches:
supported.append(("%s%s" % (impl, version), abi, arch))
# Has binaries, does not use the Python API:
for arch in arches:
supported.append(('py%s' % (versions[0][0]), 'none', arch))
# No abi / arch, but requires our implementation:
supported.append(('%s%s' % (impl, versions[0]), 'none', 'any'))
# Tagged specifically as being cross-version compatible
# (with just the major version specified)
supported.append(('%s%s' % (impl, versions[0][0]), 'none', 'any'))
# No abi / arch, generic Python
for i, version in enumerate(versions):
supported.append(('py%s' % (version,), 'none', 'any'))
if i == 0:
supported.append(('py%s' % (version[0]), 'none', 'any'))
return supported
implementation_tag = get_impl_tag()
import os import os
from pkg_resources.extern.six import binary_type
import pkg_resources.py31compat import pkg_resources.py31compat
...@@ -30,5 +31,9 @@ def build_files(file_defs, prefix=""): ...@@ -30,5 +31,9 @@ def build_files(file_defs, prefix=""):
pkg_resources.py31compat.makedirs(full_name, exist_ok=True) pkg_resources.py31compat.makedirs(full_name, exist_ok=True)
build_files(contents, prefix=full_name) build_files(contents, prefix=full_name)
else: else:
with open(full_name, 'w') as f: if isinstance(contents, binary_type):
f.write(contents) with open(full_name, 'wb') as f:
f.write(contents)
else:
with open(full_name, 'w') as f:
f.write(contents)
...@@ -110,6 +110,7 @@ class TestMetadata: ...@@ -110,6 +110,7 @@ class TestMetadata:
'[metadata]\n' '[metadata]\n'
'version = 10.1.1\n' 'version = 10.1.1\n'
'description = Some description\n' 'description = Some description\n'
'long_description_content_type = text/something\n'
'long_description = file: README\n' 'long_description = file: README\n'
'name = fake_name\n' 'name = fake_name\n'
'keywords = one, two\n' 'keywords = one, two\n'
...@@ -131,6 +132,7 @@ class TestMetadata: ...@@ -131,6 +132,7 @@ class TestMetadata:
assert metadata.version == '10.1.1' assert metadata.version == '10.1.1'
assert metadata.description == 'Some description' assert metadata.description == 'Some description'
assert metadata.long_description_content_type == 'text/something'
assert metadata.long_description == 'readme contents\nline2' assert metadata.long_description == 'readme contents\nline2'
assert metadata.provides == ['package', 'package.sub'] assert metadata.provides == ['package', 'package.sub']
assert metadata.license == 'BSD 3-Clause License' assert metadata.license == 'BSD 3-Clause License'
......
...@@ -191,7 +191,8 @@ class TestEggInfo(object): ...@@ -191,7 +191,8 @@ class TestEggInfo(object):
test_params = test.lstrip().split('\n\n', 3) test_params = test.lstrip().split('\n\n', 3)
name_kwargs = test_params.pop(0).split('\n') name_kwargs = test_params.pop(0).split('\n')
if len(name_kwargs) > 1: if len(name_kwargs) > 1:
install_cmd_kwargs = ast.literal_eval(name_kwargs[1].strip()) val = name_kwargs[1].strip()
install_cmd_kwargs = ast.literal_eval(val)
else: else:
install_cmd_kwargs = {} install_cmd_kwargs = {}
name = name_kwargs[0].strip() name = name_kwargs[0].strip()
...@@ -211,9 +212,11 @@ class TestEggInfo(object): ...@@ -211,9 +212,11 @@ class TestEggInfo(object):
expected_requires, expected_requires,
install_cmd_kwargs, install_cmd_kwargs,
marks=marks)) marks=marks))
return pytest.mark.parametrize('requires,use_setup_cfg,' return pytest.mark.parametrize(
'expected_requires,install_cmd_kwargs', 'requires,use_setup_cfg,'
argvalues, ids=idlist) 'expected_requires,install_cmd_kwargs',
argvalues, ids=idlist,
)
@RequiresTestHelper.parametrize( @RequiresTestHelper.parametrize(
# Format of a test: # Format of a test:
...@@ -227,6 +230,20 @@ class TestEggInfo(object): ...@@ -227,6 +230,20 @@ class TestEggInfo(object):
# #
# expected contents of requires.txt # expected contents of requires.txt
'''
install_requires_deterministic
install_requires=["fake-factory==0.5.2", "pytz"]
[options]
install_requires =
fake-factory==0.5.2
pytz
fake-factory==0.5.2
pytz
''',
''' '''
install_requires_with_marker install_requires_with_marker
...@@ -361,9 +378,9 @@ class TestEggInfo(object): ...@@ -361,9 +378,9 @@ class TestEggInfo(object):
mismatch_marker=mismatch_marker, mismatch_marker=mismatch_marker,
mismatch_marker_alternate=mismatch_marker_alternate, mismatch_marker_alternate=mismatch_marker_alternate,
) )
def test_requires(self, tmpdir_cwd, env, def test_requires(
requires, use_setup_cfg, self, tmpdir_cwd, env, requires, use_setup_cfg,
expected_requires, install_cmd_kwargs): expected_requires, install_cmd_kwargs):
self._setup_script_with_requires(requires, use_setup_cfg) self._setup_script_with_requires(requires, use_setup_cfg)
self._run_install_command(tmpdir_cwd, env, **install_cmd_kwargs) self._run_install_command(tmpdir_cwd, env, **install_cmd_kwargs)
egg_info_dir = os.path.join('.', 'foo.egg-info') egg_info_dir = os.path.join('.', 'foo.egg-info')
...@@ -376,6 +393,17 @@ class TestEggInfo(object): ...@@ -376,6 +393,17 @@ class TestEggInfo(object):
assert install_requires.lstrip() == expected_requires assert install_requires.lstrip() == expected_requires
assert glob.glob(os.path.join(env.paths['lib'], 'barbazquux*')) == [] assert glob.glob(os.path.join(env.paths['lib'], 'barbazquux*')) == []
def test_install_requires_unordered_disallowed(self, tmpdir_cwd, env):
"""
Packages that pass unordered install_requires sequences
should be rejected as they produce non-deterministic
builds. See #458.
"""
req = 'install_requires={"fake-factory==0.5.2", "pytz"}'
self._setup_script_with_requires(req)
with pytest.raises(AssertionError):
self._run_install_command(tmpdir_cwd, env)
def test_extras_require_with_invalid_marker(self, tmpdir_cwd, env): def test_extras_require_with_invalid_marker(self, tmpdir_cwd, env):
tmpl = 'extras_require={{":{marker}": ["barbazquux"]}},' tmpl = 'extras_require={{":{marker}": ["barbazquux"]}},'
req = tmpl.format(marker=self.invalid_marker) req = tmpl.format(marker=self.invalid_marker)
......
# -*- coding: utf-8 -*-
"""wheel tests
"""
from distutils.sysconfig import get_config_var
from distutils.util import get_platform
import contextlib
import glob
import inspect
import os
import subprocess
import sys
import pytest
from pkg_resources import Distribution, PathMetadata, PY_MAJOR
from setuptools.wheel import Wheel
from .contexts import tempdir
from .files import build_files
from .textwrap import DALS
WHEEL_INFO_TESTS = (
('invalid.whl', ValueError),
('simplewheel-2.0-1-py2.py3-none-any.whl', {
'project_name': 'simplewheel',
'version': '2.0',
'build': '1',
'py_version': 'py2.py3',
'abi': 'none',
'platform': 'any',
}),
('simple.dist-0.1-py2.py3-none-any.whl', {
'project_name': 'simple.dist',
'version': '0.1',
'build': None,
'py_version': 'py2.py3',
'abi': 'none',
'platform': 'any',
}),
('example_pkg_a-1-py3-none-any.whl', {
'project_name': 'example_pkg_a',
'version': '1',
'build': None,
'py_version': 'py3',
'abi': 'none',
'platform': 'any',
}),
('PyQt5-5.9-5.9.1-cp35.cp36.cp37-abi3-manylinux1_x86_64.whl', {
'project_name': 'PyQt5',
'version': '5.9',
'build': '5.9.1',
'py_version': 'cp35.cp36.cp37',
'abi': 'abi3',
'platform': 'manylinux1_x86_64',
}),
)
@pytest.mark.parametrize(
('filename', 'info'), WHEEL_INFO_TESTS,
ids=[t[0] for t in WHEEL_INFO_TESTS]
)
def test_wheel_info(filename, info):
if inspect.isclass(info):
with pytest.raises(info):
Wheel(filename)
return
w = Wheel(filename)
assert {k: getattr(w, k) for k in info.keys()} == info
@contextlib.contextmanager
def build_wheel(extra_file_defs=None, **kwargs):
file_defs = {
'setup.py': (DALS(
'''
# -*- coding: utf-8 -*-
from setuptools import setup
import setuptools
setup(**%r)
'''
) % kwargs).encode('utf-8'),
}
if extra_file_defs:
file_defs.update(extra_file_defs)
with tempdir() as source_dir:
build_files(file_defs, source_dir)
subprocess.check_call((sys.executable, 'setup.py',
'-q', 'bdist_wheel'), cwd=source_dir)
yield glob.glob(os.path.join(source_dir, 'dist', '*.whl'))[0]
def tree(root):
def depth(path):
return len(path.split(os.path.sep))
def prefix(path_depth):
if not path_depth:
return ''
return '| ' * (path_depth - 1) + '|-- '
lines = []
root_depth = depth(root)
for dirpath, dirnames, filenames in os.walk(root):
dirnames.sort()
filenames.sort()
dir_depth = depth(dirpath) - root_depth
if dir_depth > 0:
lines.append('%s%s/' % (prefix(dir_depth - 1),
os.path.basename(dirpath)))
for f in filenames:
lines.append('%s%s' % (prefix(dir_depth), f))
return '\n'.join(lines) + '\n'
def _check_wheel_install(filename, install_dir, install_tree,
project_name, version, requires_txt):
w = Wheel(filename)
egg_path = os.path.join(install_dir, w.egg_name())
w.install_as_egg(egg_path)
if install_tree is not None:
install_tree = install_tree.format(
py_version=PY_MAJOR,
platform=get_platform(),
shlib_ext=get_config_var('EXT_SUFFIX') or get_config_var('SO')
)
assert install_tree == tree(install_dir)
metadata = PathMetadata(egg_path, os.path.join(egg_path, 'EGG-INFO'))
dist = Distribution.from_filename(egg_path, metadata=metadata)
assert dist.project_name == project_name
assert dist.version == version
if requires_txt is None:
assert not dist.has_metadata('requires.txt')
else:
assert requires_txt == dist.get_metadata('requires.txt').lstrip()
class Record(object):
def __init__(self, id, **kwargs):
self._id = id
self._fields = kwargs
def __repr__(self):
return '%s(**%r)' % (self._id, self._fields)
WHEEL_INSTALL_TESTS = (
dict(
id='basic',
file_defs={
'foo': {
'__init__.py': ''
}
},
setup_kwargs=dict(
packages=['foo'],
),
install_tree=DALS(
'''
foo-1.0-py{py_version}.egg/
|-- EGG-INFO/
| |-- DESCRIPTION.rst
| |-- PKG-INFO
| |-- RECORD
| |-- WHEEL
| |-- metadata.json
| |-- top_level.txt
|-- foo/
| |-- __init__.py
'''
),
),
dict(
id='utf-8',
setup_kwargs=dict(
description='Description accentuée',
)
),
dict(
id='data',
file_defs={
'data.txt': DALS(
'''
Some data...
'''
),
},
setup_kwargs=dict(
data_files=[('data_dir', ['data.txt'])],
),
install_tree=DALS(
'''
foo-1.0-py{py_version}.egg/
|-- EGG-INFO/
| |-- DESCRIPTION.rst
| |-- PKG-INFO
| |-- RECORD
| |-- WHEEL
| |-- metadata.json
| |-- top_level.txt
|-- data_dir/
| |-- data.txt
'''
),
),
dict(
id='extension',
file_defs={
'extension.c': DALS(
'''
#include "Python.h"
#if PY_MAJOR_VERSION >= 3
static struct PyModuleDef moduledef = {
PyModuleDef_HEAD_INIT,
"extension",
NULL,
0,
NULL,
NULL,
NULL,
NULL,
NULL
};
#define INITERROR return NULL
PyMODINIT_FUNC PyInit_extension(void)
#else
#define INITERROR return
void initextension(void)
#endif
{
#if PY_MAJOR_VERSION >= 3
PyObject *module = PyModule_Create(&moduledef);
#else
PyObject *module = Py_InitModule("extension", NULL);
#endif
if (module == NULL)
INITERROR;
#if PY_MAJOR_VERSION >= 3
return module;
#endif
}
'''
),
},
setup_kwargs=dict(
ext_modules=[
Record('setuptools.Extension',
name='extension',
sources=['extension.c'])
],
),
install_tree=DALS(
'''
foo-1.0-py{py_version}-{platform}.egg/
|-- extension{shlib_ext}
|-- EGG-INFO/
| |-- DESCRIPTION.rst
| |-- PKG-INFO
| |-- RECORD
| |-- WHEEL
| |-- metadata.json
| |-- top_level.txt
'''
),
),
dict(
id='header',
file_defs={
'header.h': DALS(
'''
'''
),
},
setup_kwargs=dict(
headers=['header.h'],
),
install_tree=DALS(
'''
foo-1.0-py{py_version}.egg/
|-- header.h
|-- EGG-INFO/
| |-- DESCRIPTION.rst
| |-- PKG-INFO
| |-- RECORD
| |-- WHEEL
| |-- metadata.json
| |-- top_level.txt
'''
),
),
dict(
id='script',
file_defs={
'script.py': DALS(
'''
#/usr/bin/python
print('hello world!')
'''
),
'script.sh': DALS(
'''
#/bin/sh
echo 'hello world!'
'''
),
},
setup_kwargs=dict(
scripts=['script.py', 'script.sh'],
),
install_tree=DALS(
'''
foo-1.0-py{py_version}.egg/
|-- EGG-INFO/
| |-- DESCRIPTION.rst
| |-- PKG-INFO
| |-- RECORD
| |-- WHEEL
| |-- metadata.json
| |-- top_level.txt
| |-- scripts/
| | |-- script.py
| | |-- script.sh
'''
),
),
dict(
id='requires1',
install_requires='foobar==2.0',
install_tree=DALS(
'''
foo-1.0-py{py_version}.egg/
|-- EGG-INFO/
| |-- DESCRIPTION.rst
| |-- PKG-INFO
| |-- RECORD
| |-- WHEEL
| |-- metadata.json
| |-- requires.txt
| |-- top_level.txt
'''),
requires_txt=DALS(
'''
foobar==2.0
'''
),
),
dict(
id='requires2',
install_requires='''
bar
foo<=2.0; %r in sys_platform
''' % sys.platform,
requires_txt=DALS(
'''
bar
foo<=2.0
'''
),
),
dict(
id='requires3',
install_requires='''
bar; %r != sys_platform
''' % sys.platform,
),
dict(
id='requires4',
install_requires='''
foo
''',
extras_require={
'extra': 'foobar>3',
},
requires_txt=DALS(
'''
foo
[extra]
foobar>3
'''
),
),
dict(
id='requires5',
extras_require={
'extra': 'foobar; %r != sys_platform' % sys.platform,
},
requires_txt=DALS(
'''
[extra]
'''
),
),
dict(
id='namespace_package',
file_defs={
'foo': {
'bar': {
'__init__.py': ''
},
},
},
setup_kwargs=dict(
namespace_packages=['foo'],
packages=['foo.bar'],
),
install_tree=DALS(
'''
foo-1.0-py{py_version}.egg/
|-- foo-1.0-py{py_version}-nspkg.pth
|-- EGG-INFO/
| |-- DESCRIPTION.rst
| |-- PKG-INFO
| |-- RECORD
| |-- WHEEL
| |-- metadata.json
| |-- namespace_packages.txt
| |-- top_level.txt
|-- foo/
| |-- __init__.py
| |-- bar/
| | |-- __init__.py
'''),
),
dict(
id='data_in_package',
file_defs={
'foo': {
'__init__.py': '',
'data_dir': {
'data.txt': DALS(
'''
Some data...
'''
),
}
}
},
setup_kwargs=dict(
packages=['foo'],
data_files=[('foo/data_dir', ['foo/data_dir/data.txt'])],
),
install_tree=DALS(
'''
foo-1.0-py{py_version}.egg/
|-- EGG-INFO/
| |-- DESCRIPTION.rst
| |-- PKG-INFO
| |-- RECORD
| |-- WHEEL
| |-- metadata.json
| |-- top_level.txt
|-- foo/
| |-- __init__.py
| |-- data_dir/
| | |-- data.txt
'''
),
),
)
@pytest.mark.parametrize(
'params', WHEEL_INSTALL_TESTS,
ids=list(params['id'] for params in WHEEL_INSTALL_TESTS),
)
def test_wheel_install(params):
project_name = params.get('name', 'foo')
version = params.get('version', '1.0')
install_requires = params.get('install_requires', [])
extras_require = params.get('extras_require', {})
requires_txt = params.get('requires_txt', None)
install_tree = params.get('install_tree')
file_defs = params.get('file_defs', {})
setup_kwargs = params.get('setup_kwargs', {})
with build_wheel(
name=project_name,
version=version,
install_requires=install_requires,
extras_require=extras_require,
extra_file_defs=file_defs,
**setup_kwargs
) as filename, tempdir() as install_dir:
_check_wheel_install(filename, install_dir,
install_tree, project_name,
version, requires_txt)
'''Wheels support.'''
from distutils.util import get_platform
import email
import itertools
import os
import re
import zipfile
from pkg_resources import Distribution, PathMetadata, parse_version
from pkg_resources.extern.six import PY3
from setuptools import Distribution as SetuptoolsDistribution
from setuptools import pep425tags
from setuptools.command.egg_info import write_requirements
WHEEL_NAME = re.compile(
r"""^(?P<project_name>.+?)-(?P<version>\d.*?)
((-(?P<build>\d.*?))?-(?P<py_version>.+?)-(?P<abi>.+?)-(?P<platform>.+?)
)\.whl$""",
re.VERBOSE).match
NAMESPACE_PACKAGE_INIT = '''\
try:
__import__('pkg_resources').declare_namespace(__name__)
except ImportError:
__path__ = __import__('pkgutil').extend_path(__path__, __name__)
'''
def unpack(src_dir, dst_dir):
'''Move everything under `src_dir` to `dst_dir`, and delete the former.'''
for dirpath, dirnames, filenames in os.walk(src_dir):
subdir = os.path.relpath(dirpath, src_dir)
for f in filenames:
src = os.path.join(dirpath, f)
dst = os.path.join(dst_dir, subdir, f)
os.renames(src, dst)
for n, d in reversed(list(enumerate(dirnames))):
src = os.path.join(dirpath, d)
dst = os.path.join(dst_dir, subdir, d)
if not os.path.exists(dst):
# Directory does not exist in destination,
# rename it and prune it from os.walk list.
os.renames(src, dst)
del dirnames[n]
# Cleanup.
for dirpath, dirnames, filenames in os.walk(src_dir, topdown=True):
assert not filenames
os.rmdir(dirpath)
class Wheel(object):
def __init__(self, filename):
match = WHEEL_NAME(os.path.basename(filename))
if match is None:
raise ValueError('invalid wheel name: %r' % filename)
self.filename = filename
for k, v in match.groupdict().items():
setattr(self, k, v)
def tags(self):
'''List tags (py_version, abi, platform) supported by this wheel.'''
return itertools.product(self.py_version.split('.'),
self.abi.split('.'),
self.platform.split('.'))
def is_compatible(self):
'''Is the wheel is compatible with the current platform?'''
supported_tags = pep425tags.get_supported()
return next((True for t in self.tags() if t in supported_tags), False)
def egg_name(self):
return Distribution(
project_name=self.project_name, version=self.version,
platform=(None if self.platform == 'any' else get_platform()),
).egg_name() + '.egg'
def install_as_egg(self, destination_eggdir):
'''Install wheel as an egg directory.'''
with zipfile.ZipFile(self.filename) as zf:
dist_basename = '%s-%s' % (self.project_name, self.version)
dist_info = '%s.dist-info' % dist_basename
dist_data = '%s.data' % dist_basename
def get_metadata(name):
with zf.open('%s/%s' % (dist_info, name)) as fp:
value = fp.read().decode('utf-8') if PY3 else fp.read()
return email.parser.Parser().parsestr(value)
wheel_metadata = get_metadata('WHEEL')
dist_metadata = get_metadata('METADATA')
# Check wheel format version is supported.
wheel_version = parse_version(wheel_metadata.get('Wheel-Version'))
if not parse_version('1.0') <= wheel_version < parse_version('2.0dev0'):
raise ValueError('unsupported wheel format version: %s' % wheel_version)
# Extract to target directory.
os.mkdir(destination_eggdir)
zf.extractall(destination_eggdir)
# Convert metadata.
dist_info = os.path.join(destination_eggdir, dist_info)
dist = Distribution.from_location(
destination_eggdir, dist_info,
metadata=PathMetadata(destination_eggdir, dist_info)
)
# Note: we need to evaluate and strip markers now,
# as we can't easily convert back from the syntax:
# foobar; "linux" in sys_platform and extra == 'test'
def raw_req(req):
req.marker = None
return str(req)
install_requires = list(sorted(map(raw_req, dist.requires())))
extras_require = {
extra: list(sorted(
req
for req in map(raw_req, dist.requires((extra,)))
if req not in install_requires
))
for extra in dist.extras
}
egg_info = os.path.join(destination_eggdir, 'EGG-INFO')
os.rename(dist_info, egg_info)
os.rename(os.path.join(egg_info, 'METADATA'),
os.path.join(egg_info, 'PKG-INFO'))
setup_dist = SetuptoolsDistribution(attrs=dict(
install_requires=install_requires,
extras_require=extras_require,
))
write_requirements(setup_dist.get_command_obj('egg_info'),
None, os.path.join(egg_info, 'requires.txt'))
# Move data entries to their correct location.
dist_data = os.path.join(destination_eggdir, dist_data)
dist_data_scripts = os.path.join(dist_data, 'scripts')
if os.path.exists(dist_data_scripts):
egg_info_scripts = os.path.join(destination_eggdir,
'EGG-INFO', 'scripts')
os.mkdir(egg_info_scripts)
for entry in os.listdir(dist_data_scripts):
# Remove bytecode, as it's not properly handled
# during easy_install scripts install phase.
if entry.endswith('.pyc'):
os.unlink(os.path.join(dist_data_scripts, entry))
else:
os.rename(os.path.join(dist_data_scripts, entry),
os.path.join(egg_info_scripts, entry))
os.rmdir(dist_data_scripts)
for subdir in filter(os.path.exists, (
os.path.join(dist_data, d)
for d in ('data', 'headers', 'purelib', 'platlib')
)):
unpack(subdir, destination_eggdir)
if os.path.exists(dist_data):
os.rmdir(dist_data)
# Fix namespace packages.
namespace_packages = os.path.join(egg_info, 'namespace_packages.txt')
if os.path.exists(namespace_packages):
with open(namespace_packages) as fp:
namespace_packages = fp.read().split()
for mod in namespace_packages:
mod_dir = os.path.join(destination_eggdir, *mod.split('.'))
mod_init = os.path.join(mod_dir, '__init__.py')
if os.path.exists(mod_dir) and not os.path.exists(mod_init):
with open(mod_init, 'w') as fp:
fp.write(NAMESPACE_PACKAGE_INIT)
...@@ -3,4 +3,5 @@ mock ...@@ -3,4 +3,5 @@ mock
pytest-flake8; python_version>="2.7" pytest-flake8; python_version>="2.7"
virtualenv>=13.0.0 virtualenv>=13.0.0
pytest-virtualenv>=1.2.7 pytest-virtualenv>=1.2.7
pytest>=3.0.2 pytest>=3.0.2,<=3.2.5
wheel
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment