summaryrefslogtreecommitdiff
path: root/setuptools
diff options
context:
space:
mode:
authorJason R. Coombs <jaraco@jaraco.com>2017-09-03 19:57:54 -0400
committerJason R. Coombs <jaraco@jaraco.com>2017-09-03 20:01:45 -0400
commitdcb24ad15465c266a3f258471766fbbe8fc8a42e (patch)
tree13123440610d78e398476a8ce1e8cc3d9f9ec72e /setuptools
parentf14930e66601b462699c44384c482cd966f53b8f (diff)
parent1b192005562d5cf0de30c02154c58fd1dca577c8 (diff)
downloadpython-setuptools-git-dcb24ad15465c266a3f258471766fbbe8fc8a42e.tar.gz
Merge branch 'master' into drop-py26
Diffstat (limited to 'setuptools')
-rw-r--r--setuptools/__init__.py2
-rw-r--r--setuptools/command/__init__.py2
-rw-r--r--setuptools/command/bdist_egg.py16
-rw-r--r--setuptools/command/build_clib.py98
-rwxr-xr-xsetuptools/command/develop.py33
-rwxr-xr-xsetuptools/command/easy_install.py46
-rwxr-xr-xsetuptools/command/egg_info.py59
-rwxr-xr-xsetuptools/command/sdist.py3
-rw-r--r--setuptools/command/test.py21
-rw-r--r--setuptools/command/upload.py4
-rw-r--r--setuptools/command/upload_docs.py26
-rw-r--r--setuptools/config.py133
-rw-r--r--setuptools/dep_util.py23
-rw-r--r--setuptools/depends.py43
-rw-r--r--setuptools/dist.py192
-rw-r--r--setuptools/monkey.py37
-rw-r--r--setuptools/msvc.py261
-rwxr-xr-xsetuptools/namespaces.py20
-rwxr-xr-xsetuptools/package_index.py14
-rw-r--r--setuptools/py27compat.py16
-rw-r--r--setuptools/py33compat.py45
-rw-r--r--setuptools/py36compat.py82
-rwxr-xr-xsetuptools/sandbox.py73
-rw-r--r--setuptools/ssl_support.py55
-rw-r--r--setuptools/tests/__init__.py4
-rw-r--r--setuptools/tests/files.py6
-rw-r--r--setuptools/tests/mod_with_constant.py1
-rw-r--r--setuptools/tests/test_bdist_egg.py2
-rw-r--r--setuptools/tests/test_build_clib.py59
-rw-r--r--setuptools/tests/test_config.py136
-rw-r--r--setuptools/tests/test_dep_util.py30
-rw-r--r--setuptools/tests/test_depends.py16
-rw-r--r--setuptools/tests/test_develop.py75
-rw-r--r--setuptools/tests/test_dist.py46
-rw-r--r--setuptools/tests/test_easy_install.py25
-rw-r--r--setuptools/tests/test_egg_info.py285
-rw-r--r--setuptools/tests/test_integration.py65
-rw-r--r--setuptools/tests/test_manifest.py128
-rw-r--r--setuptools/tests/test_msvc.py5
-rw-r--r--setuptools/tests/test_namespaces.py63
-rw-r--r--setuptools/tests/test_packageindex.py42
-rw-r--r--setuptools/tests/test_sandbox.py13
-rw-r--r--setuptools/tests/test_upload_docs.py2
-rw-r--r--setuptools/tests/test_virtualenv.py116
44 files changed, 1923 insertions, 500 deletions
diff --git a/setuptools/__init__.py b/setuptools/__init__.py
index 54577ced..04f76740 100644
--- a/setuptools/__init__.py
+++ b/setuptools/__init__.py
@@ -7,7 +7,7 @@ import distutils.filelist
from distutils.util import convert_path
from fnmatch import fnmatchcase
-from setuptools.extern.six.moves import filter, filterfalse, map
+from setuptools.extern.six.moves import filter, map
import setuptools.version
from setuptools.extension import Extension
diff --git a/setuptools/command/__init__.py b/setuptools/command/__init__.py
index efbe9411..c96d33c2 100644
--- a/setuptools/command/__init__.py
+++ b/setuptools/command/__init__.py
@@ -2,7 +2,7 @@ __all__ = [
'alias', 'bdist_egg', 'bdist_rpm', 'build_ext', 'build_py', 'develop',
'easy_install', 'egg_info', 'install', 'install_lib', 'rotate', 'saveopts',
'sdist', 'setopt', 'test', 'install_egg_info', 'install_scripts',
- 'register', 'bdist_wininst', 'upload_docs', 'upload',
+ 'register', 'bdist_wininst', 'upload_docs', 'upload', 'build_clib',
]
from distutils.command.bdist import bdist
diff --git a/setuptools/command/bdist_egg.py b/setuptools/command/bdist_egg.py
index 8cd9dfef..51755d52 100644
--- a/setuptools/command/bdist_egg.py
+++ b/setuptools/command/bdist_egg.py
@@ -38,6 +38,14 @@ def strip_module(filename):
filename = filename[:-6]
return filename
+def sorted_walk(dir):
+ """Do os.walk in a reproducible way,
+ independent of indeterministic filesystem readdir order
+ """
+ for base, dirs, files in os.walk(dir):
+ dirs.sort()
+ files.sort()
+ yield base, dirs, files
def write_stub(resource, pyfile):
_stub_template = textwrap.dedent("""
@@ -302,7 +310,7 @@ class bdist_egg(Command):
ext_outputs = []
paths = {self.bdist_dir: ''}
- for base, dirs, files in os.walk(self.bdist_dir):
+ for base, dirs, files in sorted_walk(self.bdist_dir):
for filename in files:
if os.path.splitext(filename)[1].lower() in NATIVE_EXTENSIONS:
all_outputs.append(paths[base] + filename)
@@ -329,7 +337,7 @@ NATIVE_EXTENSIONS = dict.fromkeys('.dll .so .dylib .pyd'.split())
def walk_egg(egg_dir):
"""Walk an unpacked egg's contents, skipping the metadata directory"""
- walker = os.walk(egg_dir)
+ walker = sorted_walk(egg_dir)
base, dirs, files = next(walker)
if 'EGG-INFO' in dirs:
dirs.remove('EGG-INFO')
@@ -463,10 +471,10 @@ def make_zipfile(zip_filename, base_dir, verbose=0, dry_run=0, compress=True,
compression = zipfile.ZIP_DEFLATED if compress else zipfile.ZIP_STORED
if not dry_run:
z = zipfile.ZipFile(zip_filename, mode, compression=compression)
- for dirname, dirs, files in os.walk(base_dir):
+ for dirname, dirs, files in sorted_walk(base_dir):
visit(z, dirname, files)
z.close()
else:
- for dirname, dirs, files in os.walk(base_dir):
+ for dirname, dirs, files in sorted_walk(base_dir):
visit(None, dirname, files)
return zip_filename
diff --git a/setuptools/command/build_clib.py b/setuptools/command/build_clib.py
new file mode 100644
index 00000000..09caff6f
--- /dev/null
+++ b/setuptools/command/build_clib.py
@@ -0,0 +1,98 @@
+import distutils.command.build_clib as orig
+from distutils.errors import DistutilsSetupError
+from distutils import log
+from setuptools.dep_util import newer_pairwise_group
+
+
+class build_clib(orig.build_clib):
+ """
+ Override the default build_clib behaviour to do the following:
+
+ 1. Implement a rudimentary timestamp-based dependency system
+ so 'compile()' doesn't run every time.
+ 2. Add more keys to the 'build_info' dictionary:
+ * obj_deps - specify dependencies for each object compiled.
+ this should be a dictionary mapping a key
+ with the source filename to a list of
+ dependencies. Use an empty string for global
+ dependencies.
+ * cflags - specify a list of additional flags to pass to
+ the compiler.
+ """
+
+ def build_libraries(self, libraries):
+ for (lib_name, build_info) in libraries:
+ sources = build_info.get('sources')
+ if sources is None or not isinstance(sources, (list, tuple)):
+ raise DistutilsSetupError(
+ "in 'libraries' option (library '%s'), "
+ "'sources' must be present and must be "
+ "a list of source filenames" % lib_name)
+ sources = list(sources)
+
+ log.info("building '%s' library", lib_name)
+
+ # Make sure everything is the correct type.
+ # obj_deps should be a dictionary of keys as sources
+ # and a list/tuple of files that are its dependencies.
+ obj_deps = build_info.get('obj_deps', dict())
+ if not isinstance(obj_deps, dict):
+ raise DistutilsSetupError(
+ "in 'libraries' option (library '%s'), "
+ "'obj_deps' must be a dictionary of "
+ "type 'source: list'" % lib_name)
+ dependencies = []
+
+ # Get the global dependencies that are specified by the '' key.
+ # These will go into every source's dependency list.
+ global_deps = obj_deps.get('', list())
+ if not isinstance(global_deps, (list, tuple)):
+ raise DistutilsSetupError(
+ "in 'libraries' option (library '%s'), "
+ "'obj_deps' must be a dictionary of "
+ "type 'source: list'" % lib_name)
+
+ # Build the list to be used by newer_pairwise_group
+ # each source will be auto-added to its dependencies.
+ for source in sources:
+ src_deps = [source]
+ src_deps.extend(global_deps)
+ extra_deps = obj_deps.get(source, list())
+ if not isinstance(extra_deps, (list, tuple)):
+ raise DistutilsSetupError(
+ "in 'libraries' option (library '%s'), "
+ "'obj_deps' must be a dictionary of "
+ "type 'source: list'" % lib_name)
+ src_deps.extend(extra_deps)
+ dependencies.append(src_deps)
+
+ expected_objects = self.compiler.object_filenames(
+ sources,
+ output_dir=self.build_temp
+ )
+
+ if newer_pairwise_group(dependencies, expected_objects) != ([], []):
+ # First, compile the source code to object files in the library
+ # directory. (This should probably change to putting object
+ # files in a temporary build directory.)
+ macros = build_info.get('macros')
+ include_dirs = build_info.get('include_dirs')
+ cflags = build_info.get('cflags')
+ objects = self.compiler.compile(
+ sources,
+ output_dir=self.build_temp,
+ macros=macros,
+ include_dirs=include_dirs,
+ extra_postargs=cflags,
+ debug=self.debug
+ )
+
+ # Now "link" the object files together into a static library.
+ # (On Unix at least, this isn't really linking -- it just
+ # builds an archive. Whatever.)
+ self.compiler.create_static_lib(
+ expected_objects,
+ lib_name,
+ output_dir=self.build_clib,
+ debug=self.debug
+ )
diff --git a/setuptools/command/develop.py b/setuptools/command/develop.py
index 3eb86120..85b23c60 100755
--- a/setuptools/command/develop.py
+++ b/setuptools/command/develop.py
@@ -9,10 +9,11 @@ from setuptools.extern import six
from pkg_resources import Distribution, PathMetadata, normalize_path
from setuptools.command.easy_install import easy_install
+from setuptools import namespaces
import setuptools
-class develop(easy_install):
+class develop(namespaces.DevelopInstaller, easy_install):
"""Set up package for development"""
description = "install package in 'development mode'"
@@ -30,6 +31,7 @@ class develop(easy_install):
if self.uninstall:
self.multi_version = True
self.uninstall_link()
+ self.uninstall_namespaces()
else:
self.install_for_development()
self.warn_deprecated_options()
@@ -77,15 +79,28 @@ class develop(easy_install):
project_name=ei.egg_name
)
- p = self.egg_base.replace(os.sep, '/')
- if p != os.curdir:
- p = '../' * (p.count('/') + 1)
- self.setup_path = p
- p = normalize_path(os.path.join(self.install_dir, self.egg_path, p))
- if p != normalize_path(os.curdir):
+ self.setup_path = self._resolve_setup_path(
+ self.egg_base,
+ self.install_dir,
+ self.egg_path,
+ )
+
+ @staticmethod
+ def _resolve_setup_path(egg_base, install_dir, egg_path):
+ """
+ Generate a path from egg_base back to '.' where the
+ setup script resides and ensure that path points to the
+ setup path from $install_dir/$egg_path.
+ """
+ path_to_setup = egg_base.replace(os.sep, '/').rstrip('/')
+ if path_to_setup != os.curdir:
+ path_to_setup = '../' * (path_to_setup.count('/') + 1)
+ resolved = normalize_path(os.path.join(install_dir, egg_path, path_to_setup))
+ if resolved != normalize_path(os.curdir):
raise DistutilsOptionError(
"Can't get a consistent path to setup script from"
- " installation directory", p, normalize_path(os.curdir))
+ " installation directory", resolved, normalize_path(os.curdir))
+ return path_to_setup
def install_for_development(self):
if six.PY3 and getattr(self.distribution, 'use_2to3', False):
@@ -123,6 +138,8 @@ class develop(easy_install):
self.easy_install(setuptools.bootstrap_install_from)
setuptools.bootstrap_install_from = None
+ self.install_namespaces()
+
# create an .egg-link in the installation dir, pointing to our egg
log.info("Creating %s (link to %s)", self.egg_link, self.egg_base)
if not self.dry_run:
diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py
index 03dd6768..8fba7b41 100755
--- a/setuptools/command/easy_install.py
+++ b/setuptools/command/easy_install.py
@@ -46,6 +46,7 @@ from setuptools.extern.six.moves import configparser, map
from setuptools import Command
from setuptools.sandbox import run_setup
from setuptools.py31compat import get_path, get_config_vars
+from setuptools.py27compat import rmtree_safe
from setuptools.command import setopt
from setuptools.archive_util import unpack_archive
from setuptools.package_index import (
@@ -58,7 +59,7 @@ from pkg_resources import (
Distribution, PathMetadata, EggMetadata, WorkingSet, DistributionNotFound,
VersionConflict, DEVELOP_DIST,
)
-import pkg_resources
+import pkg_resources.py31compat
# Turn on PEP440Warnings
warnings.filterwarnings("default", category=pkg_resources.PEP440Warning)
@@ -473,8 +474,7 @@ class easy_install(Command):
else:
self.pth_file = None
- PYTHONPATH = os.environ.get('PYTHONPATH', '').split(os.pathsep)
- if instdir not in map(normalize_path, filter(None, PYTHONPATH)):
+ if instdir not in map(normalize_path, _pythonpath()):
# only PYTHONPATH dirs need a site.py, so pretend it's there
self.sitepy_installed = True
elif self.multi_version and not os.path.exists(pth_file):
@@ -544,8 +544,7 @@ class easy_install(Command):
if ok_exists:
os.unlink(ok_file)
dirname = os.path.dirname(ok_file)
- if not os.path.exists(dirname):
- os.makedirs(dirname)
+ pkg_resources.py31compat.makedirs(dirname, exist_ok=True)
f = open(pth_file, 'w')
except (OSError, IOError):
self.cant_write_to_target()
@@ -627,12 +626,20 @@ class easy_install(Command):
(spec.key, self.build_directory)
)
+ @contextlib.contextmanager
+ def _tmpdir(self):
+ tmpdir = tempfile.mkdtemp(prefix=six.u("easy_install-"))
+ try:
+ # cast to str as workaround for #709 and #710 and #712
+ yield str(tmpdir)
+ finally:
+ os.path.exists(tmpdir) and rmtree(rmtree_safe(tmpdir))
+
def easy_install(self, spec, deps=False):
- tmpdir = tempfile.mkdtemp(prefix="easy_install-")
if not self.editable:
self.install_site_py()
- try:
+ with self._tmpdir() as tmpdir:
if not isinstance(spec, Requirement):
if URL_SCHEME(spec):
# It's a url, download it to tmpdir and process
@@ -664,10 +671,6 @@ class easy_install(Command):
else:
return self.install_item(spec, dist.location, tmpdir, deps)
- finally:
- if os.path.exists(tmpdir):
- rmtree(tmpdir)
-
def install_item(self, spec, download, tmpdir, deps, install_needed=False):
# Installation is also needed if file in tmpdir or is not an egg
@@ -1343,10 +1346,21 @@ class easy_install(Command):
setattr(self, attr, val)
+def _pythonpath():
+ items = os.environ.get('PYTHONPATH', '').split(os.pathsep)
+ return filter(None, items)
+
+
def get_site_dirs():
- # return a list of 'site' dirs
- sitedirs = [_f for _f in os.environ.get('PYTHONPATH',
- '').split(os.pathsep) if _f]
+ """
+ Return a list of 'site' dirs
+ """
+
+ sitedirs = []
+
+ # start with PYTHONPATH
+ sitedirs.extend(_pythonpath())
+
prefixes = [sys.prefix]
if sys.exec_prefix != sys.prefix:
prefixes.append(sys.exec_prefix)
@@ -1670,7 +1684,7 @@ def _first_line_re():
def auto_chmod(func, arg, exc):
- if func is os.remove and os.name == 'nt':
+ if func in [os.unlink, os.remove] and os.name == 'nt':
chmod(arg, stat.S_IWRITE)
return func(arg)
et, ev, _ = sys.exc_info()
@@ -2008,7 +2022,7 @@ class ScriptWriter(object):
gui apps.
"""
- template = textwrap.dedent("""
+ template = textwrap.dedent(r"""
# EASY-INSTALL-ENTRY-SCRIPT: %(spec)r,%(group)r,%(name)r
__requires__ = %(spec)r
import re
diff --git a/setuptools/command/egg_info.py b/setuptools/command/egg_info.py
index 40cea9bf..a1d41b27 100755
--- a/setuptools/command/egg_info.py
+++ b/setuptools/command/egg_info.py
@@ -32,11 +32,6 @@ from setuptools.glob import glob
from pkg_resources.extern import packaging
-try:
- from setuptools_svn import svn_utils
-except ImportError:
- pass
-
def translate_pattern(glob):
"""
@@ -117,7 +112,8 @@ def translate_pattern(glob):
if not last_chunk:
pat += sep
- return re.compile(pat + r'\Z(?ms)')
+ pat += r'\Z'
+ return re.compile(pat, flags=re.MULTILINE|re.DOTALL)
class egg_info(Command):
@@ -126,18 +122,13 @@ class egg_info(Command):
user_options = [
('egg-base=', 'e', "directory containing .egg-info directories"
" (default: top of the source tree)"),
- ('tag-svn-revision', 'r',
- "Add subversion revision ID to version number"),
('tag-date', 'd', "Add date stamp (e.g. 20050528) to version number"),
('tag-build=', 'b', "Specify explicit tag to add to version number"),
- ('no-svn-revision', 'R',
- "Don't add subversion revision ID [default]"),
('no-date', 'D', "Don't include date stamp [default]"),
]
- boolean_options = ['tag-date', 'tag-svn-revision']
+ boolean_options = ['tag-date']
negative_opt = {
- 'no-svn-revision': 'tag-svn-revision',
'no-date': 'tag-date',
}
@@ -147,15 +138,26 @@ class egg_info(Command):
self.egg_base = None
self.egg_info = None
self.tag_build = None
- self.tag_svn_revision = 0
self.tag_date = 0
self.broken_egg_info = False
self.vtags = None
+ ####################################
+ # allow the 'tag_svn_revision' to be detected and
+ # set, supporting sdists built on older Setuptools.
+ @property
+ def tag_svn_revision(self):
+ pass
+
+ @tag_svn_revision.setter
+ def tag_svn_revision(self, value):
+ pass
+ ####################################
+
def save_version_info(self, filename):
"""
- Materialize the values of svn_revision and date into the
- build tag. Install these keys in a deterministic order
+ Materialize the value of date into the
+ build tag. Install build keys in a deterministic order
to avoid arbitrary reordering on subsequent builds.
"""
egg_info = collections.OrderedDict()
@@ -163,7 +165,6 @@ class egg_info(Command):
# when PYTHONHASHSEED=0
egg_info['tag_build'] = self.tags()
egg_info['tag_date'] = 0
- egg_info['tag_svn_revision'] = 0
edit_config(filename, dict(egg_info=egg_info))
def finalize_options(self):
@@ -280,22 +281,10 @@ class egg_info(Command):
version = ''
if self.tag_build:
version += self.tag_build
- if self.tag_svn_revision:
- warnings.warn(
- "tag_svn_revision is deprecated and will not be honored "
- "in a future release"
- )
- version += '-r%s' % self.get_svn_revision()
if self.tag_date:
version += time.strftime("-%Y%m%d")
return version
- @staticmethod
- def get_svn_revision():
- if 'svn_utils' not in globals():
- return "0"
- return str(svn_utils.SvnInfo.load(os.curdir).get_revision())
-
def find_sources(self):
"""Generate SOURCES.txt manifest file"""
manifest_filename = os.path.join(self.egg_info, "SOURCES.txt")
@@ -439,7 +428,11 @@ class FileList(_FileList):
def graft(self, dir):
"""Include all files from 'dir/'."""
- found = distutils.filelist.findall(dir)
+ found = [
+ item
+ for match_dir in glob(dir)
+ for item in distutils.filelist.findall(match_dir)
+ ]
self.extend(found)
return bool(found)
@@ -455,7 +448,7 @@ class FileList(_FileList):
"""
if self.allfiles is None:
self.findall()
- match = translate_pattern(os.path.join('**', '*' + pattern))
+ match = translate_pattern(os.path.join('**', pattern))
found = [f for f in self.allfiles if match.match(f)]
self.extend(found)
return bool(found)
@@ -464,7 +457,7 @@ class FileList(_FileList):
"""
Exclude all files anywhere that match the pattern.
"""
- match = translate_pattern(os.path.join('**', '*' + pattern))
+ match = translate_pattern(os.path.join('**', pattern))
return self._remove_files(match.match)
def append(self, item):
@@ -604,6 +597,10 @@ def write_pkg_info(cmd, basename, filename):
metadata = cmd.distribution.metadata
metadata.version, oldver = cmd.egg_version, metadata.version
metadata.name, oldname = cmd.egg_name, metadata.name
+ metadata.long_description_content_type = getattr(
+ cmd.distribution,
+ 'long_description_content_type'
+ )
try:
# write unescaped data to PKG-INFO, so older pkg_resources
# can still parse it
diff --git a/setuptools/command/sdist.py b/setuptools/command/sdist.py
index 39e29d73..bcfae4d8 100755
--- a/setuptools/command/sdist.py
+++ b/setuptools/command/sdist.py
@@ -37,7 +37,8 @@ class sdist(sdist_add_defaults, orig.sdist):
negative_opt = {}
- READMES = 'README', 'README.rst', 'README.txt'
+ README_EXTENSIONS = ['', '.rst', '.txt', '.md']
+ READMES = tuple('README{0}'.format(ext) for ext in README_EXTENSIONS)
def run(self):
self.run_command('egg_info')
diff --git a/setuptools/command/test.py b/setuptools/command/test.py
index 9931565b..f00d6794 100644
--- a/setuptools/command/test.py
+++ b/setuptools/command/test.py
@@ -4,14 +4,15 @@ import sys
import contextlib
import itertools
import unittest
-from distutils.errors import DistutilsOptionError
+from distutils.errors import DistutilsError, DistutilsOptionError
+from distutils import log
from unittest import TestLoader
from setuptools.extern import six
from setuptools.extern.six.moves import map, filter
from pkg_resources import (resource_listdir, resource_exists, normalize_path,
- working_set, _namespace_packages,
+ working_set, _namespace_packages, evaluate_marker,
add_activation_listener, require, EntryPoint)
from setuptools import Command
@@ -66,7 +67,7 @@ class test(Command):
user_options = [
('test-module=', 'm', "Run 'test_suite' in specified module"),
('test-suite=', 's',
- "Test suite to run (e.g. 'some_module.test_suite')"),
+ "Run single test, case or suite (e.g. 'module.test_suite')"),
('test-runner=', 'r', "Test runner to use"),
]
@@ -190,9 +191,13 @@ class test(Command):
Install the requirements indicated by self.distribution and
return an iterable of the dists that were built.
"""
- ir_d = dist.fetch_build_eggs(dist.install_requires or [])
+ ir_d = dist.fetch_build_eggs(dist.install_requires)
tr_d = dist.fetch_build_eggs(dist.tests_require or [])
- return itertools.chain(ir_d, tr_d)
+ er_d = dist.fetch_build_eggs(
+ v for k, v in dist.extras_require.items()
+ if k.startswith(':') and evaluate_marker(k[1:])
+ )
+ return itertools.chain(ir_d, tr_d, er_d)
def run(self):
installed_dists = self.install_dists(self.distribution)
@@ -225,12 +230,16 @@ class test(Command):
del_modules.append(name)
list(map(sys.modules.__delitem__, del_modules))
- unittest.main(
+ test = unittest.main(
None, None, self._argv,
testLoader=self._resolve_as_ep(self.test_loader),
testRunner=self._resolve_as_ep(self.test_runner),
exit=False,
)
+ if not test.result.wasSuccessful():
+ msg = 'Test failed: %s' % test.result
+ self.announce(msg, log.ERROR)
+ raise DistutilsError(msg)
@property
def _argv(self):
diff --git a/setuptools/command/upload.py b/setuptools/command/upload.py
index 484baa5a..a44173a9 100644
--- a/setuptools/command/upload.py
+++ b/setuptools/command/upload.py
@@ -10,6 +10,10 @@ class upload(orig.upload):
def finalize_options(self):
orig.upload.finalize_options(self)
+ self.username = (
+ self.username or
+ getpass.getuser()
+ )
# Attempt to obtain password. Short circuit evaluation at the first
# sign of success.
self.password = (
diff --git a/setuptools/command/upload_docs.py b/setuptools/command/upload_docs.py
index 269dc2d5..07aa564a 100644
--- a/setuptools/command/upload_docs.py
+++ b/setuptools/command/upload_docs.py
@@ -57,7 +57,6 @@ class upload_docs(upload):
self.target_dir = None
def finalize_options(self):
- log.warn("Upload_docs command is deprecated. Use RTD instead.")
upload.finalize_options(self)
if self.upload_dir is None:
if self.has_sphinx():
@@ -69,6 +68,8 @@ class upload_docs(upload):
else:
self.ensure_dirname('upload_dir')
self.target_dir = self.upload_dir
+ if 'pypi.python.org' in self.repository:
+ log.warn("Upload_docs command is deprecated. Use RTD instead.")
self.announce('Using upload directory %s' % self.target_dir)
def create_zipfile(self, filename):
@@ -77,9 +78,8 @@ class upload_docs(upload):
self.mkpath(self.target_dir) # just in case
for root, dirs, files in os.walk(self.target_dir):
if root == self.target_dir and not files:
- raise DistutilsOptionError(
- "no files found in upload directory '%s'"
- % self.target_dir)
+ tmpl = "no files found in upload directory '%s'"
+ raise DistutilsOptionError(tmpl % self.target_dir)
for name in files:
full = os.path.join(root, name)
relative = root[len(self.target_dir):].lstrip(os.path.sep)
@@ -138,7 +138,7 @@ class upload_docs(upload):
part_groups = map(builder, data.items())
parts = itertools.chain.from_iterable(part_groups)
body_items = itertools.chain(parts, end_items)
- content_type = 'multipart/form-data; boundary=%s' % boundary
+ content_type = 'multipart/form-data; boundary=%s' % boundary.decode('ascii')
return b''.join(body_items), content_type
def upload_file(self, filename):
@@ -159,8 +159,8 @@ class upload_docs(upload):
body, ct = self._build_multipart(data)
- self.announce("Submitting documentation to %s" % (self.repository),
- log.INFO)
+ msg = "Submitting documentation to %s" % (self.repository)
+ self.announce(msg, log.INFO)
# build the Request
# We can't use urllib2 since we need to send the Basic
@@ -191,16 +191,16 @@ class upload_docs(upload):
r = conn.getresponse()
if r.status == 200:
- self.announce('Server response (%s): %s' % (r.status, r.reason),
- log.INFO)
+ msg = 'Server response (%s): %s' % (r.status, r.reason)
+ self.announce(msg, log.INFO)
elif r.status == 301:
location = r.getheader('Location')
if location is None:
location = 'https://pythonhosted.org/%s/' % meta.get_name()
- self.announce('Upload successful. Visit %s' % location,
- log.INFO)
+ msg = 'Upload successful. Visit %s' % location
+ self.announce(msg, log.INFO)
else:
- self.announce('Upload failed (%s): %s' % (r.status, r.reason),
- log.ERROR)
+ msg = 'Upload failed (%s): %s' % (r.status, r.reason)
+ self.announce(msg, log.ERROR)
if self.show_response:
print('-' * 75, r.read(), '-' * 75)
diff --git a/setuptools/config.py b/setuptools/config.py
index eb19c895..53828447 100644
--- a/setuptools/config.py
+++ b/setuptools/config.py
@@ -10,7 +10,8 @@ from distutils.errors import DistutilsOptionError, DistutilsFileError
from setuptools.extern.six import string_types
-def read_configuration(filepath, find_others=False):
+def read_configuration(
+ filepath, find_others=False, ignore_option_errors=False):
"""Read given configuration file and returns options from it as a dict.
:param str|unicode filepath: Path to configuration file
@@ -19,6 +20,11 @@ def read_configuration(filepath, find_others=False):
:param bool find_others: Whether to search for other configuration files
which could be on in various places.
+ :param bool ignore_option_errors: Whether to silently ignore
+ options, values of which could not be resolved (e.g. due to exceptions
+ in directives such as file:, attr:, etc.).
+ If False exceptions are propagated as expected.
+
:rtype: dict
"""
from setuptools.dist import Distribution, _Distribution
@@ -32,17 +38,21 @@ def read_configuration(filepath, find_others=False):
current_directory = os.getcwd()
os.chdir(os.path.dirname(filepath))
- dist = Distribution()
+ try:
+ dist = Distribution()
- filenames = dist.find_config_files() if find_others else []
- if filepath not in filenames:
- filenames.append(filepath)
+ filenames = dist.find_config_files() if find_others else []
+ if filepath not in filenames:
+ filenames.append(filepath)
- _Distribution.parse_config_files(dist, filenames=filenames)
+ _Distribution.parse_config_files(dist, filenames=filenames)
- handlers = parse_configuration(dist, dist.command_options)
+ handlers = parse_configuration(
+ dist, dist.command_options,
+ ignore_option_errors=ignore_option_errors)
- os.chdir(current_directory)
+ finally:
+ os.chdir(current_directory)
return configuration_to_dict(handlers)
@@ -76,7 +86,8 @@ def configuration_to_dict(handlers):
return config_dict
-def parse_configuration(distribution, command_options):
+def parse_configuration(
+ distribution, command_options, ignore_option_errors=False):
"""Performs additional parsing of configuration options
for a distribution.
@@ -84,12 +95,18 @@ def parse_configuration(distribution, command_options):
:param Distribution distribution:
:param dict command_options:
+ :param bool ignore_option_errors: Whether to silently ignore
+ options, values of which could not be resolved (e.g. due to exceptions
+ in directives such as file:, attr:, etc.).
+ If False exceptions are propagated as expected.
:rtype: list
"""
- meta = ConfigMetadataHandler(distribution.metadata, command_options)
+ meta = ConfigMetadataHandler(
+ distribution.metadata, command_options, ignore_option_errors)
meta.parse()
- options = ConfigOptionsHandler(distribution, command_options)
+ options = ConfigOptionsHandler(
+ distribution, command_options, ignore_option_errors)
options.parse()
return [meta, options]
@@ -111,7 +128,7 @@ class ConfigHandler(object):
"""
- def __init__(self, target_obj, options):
+ def __init__(self, target_obj, options, ignore_option_errors=False):
sections = {}
section_prefix = self.section_prefix
@@ -122,6 +139,7 @@ class ConfigHandler(object):
section_name = section_name.replace(section_prefix, '').strip('.')
sections[section_name] = section_options
+ self.ignore_option_errors = ignore_option_errors
self.target_obj = target_obj
self.sections = sections
self.set_options = []
@@ -148,9 +166,19 @@ class ConfigHandler(object):
# Already inhabited. Skipping.
return
+ skip_option = False
parser = self.parsers.get(option_name)
if parser:
- value = parser(value)
+ try:
+ value = parser(value)
+
+ except Exception:
+ skip_option = True
+ if not self.ignore_option_errors:
+ raise
+
+ if skip_option:
+ return
setter = getattr(target_obj, 'set_%s' % option_name, None)
if setter is None:
@@ -217,33 +245,39 @@ class ConfigHandler(object):
directory with setup.py.
Examples:
- include: LICENSE
- include: src/file.txt
+ file: LICENSE
+ file: README.rst, CHANGELOG.md, src/file.txt
:param str value:
:rtype: str
"""
+ include_directive = 'file:'
+
if not isinstance(value, string_types):
return value
- include_directive = 'file:'
if not value.startswith(include_directive):
return value
- current_directory = os.getcwd()
-
- filepath = value.replace(include_directive, '').strip()
- filepath = os.path.abspath(filepath)
-
- if not filepath.startswith(current_directory):
+ spec = value[len(include_directive):]
+ filepaths = (os.path.abspath(path.strip()) for path in spec.split(','))
+ return '\n'.join(
+ cls._read_file(path)
+ for path in filepaths
+ if (cls._assert_local(path) or True)
+ and os.path.isfile(path)
+ )
+
+ @staticmethod
+ def _assert_local(filepath):
+ if not filepath.startswith(os.getcwd()):
raise DistutilsOptionError(
'`file:` directive can not access %s' % filepath)
- if os.path.isfile(filepath):
- with io.open(filepath, encoding='utf-8') as f:
- value = f.read()
-
- return value
+ @staticmethod
+ def _read_file(filepath):
+ with io.open(filepath, encoding='utf-8') as f:
+ return f.read()
@classmethod
def _parse_attr(cls, value):
@@ -335,7 +369,10 @@ class ConfigHandler(object):
method_postfix = '_%s' % section_name
section_parser_method = getattr(
- self, 'parse_section%s' % method_postfix, None)
+ self,
+ # Dots in section names are tranlsated into dunderscores.
+ ('parse_section%s' % method_postfix).replace('.', '__'),
+ None)
if section_parser_method is None:
raise DistutilsOptionError(
@@ -381,17 +418,6 @@ class ConfigMetadataHandler(ConfigHandler):
'version': self._parse_version,
}
- def parse_section_classifiers(self, section_options):
- """Parses configuration file section.
-
- :param dict section_options:
- """
- classifiers = []
- for begin, (_, rest) in section_options.items():
- classifiers.append('%s :%s' % (begin.title(), rest))
-
- self['classifiers'] = classifiers
-
def _parse_version(self, value):
"""Parses `version` option value.
@@ -442,6 +468,7 @@ class ConfigOptionsHandler(ConfigHandler):
'tests_require': parse_list_semicolon,
'packages': self._parse_packages,
'entry_points': self._parse_file,
+ 'py_modules': parse_list,
}
def _parse_packages(self, value):
@@ -455,8 +482,34 @@ class ConfigOptionsHandler(ConfigHandler):
if not value.startswith(find_directive):
return self._parse_list(value)
+ # Read function arguments from a dedicated section.
+ find_kwargs = self.parse_section_packages__find(
+ self.sections.get('packages.find', {}))
+
from setuptools import find_packages
- return find_packages()
+
+ return find_packages(**find_kwargs)
+
+ def parse_section_packages__find(self, section_options):
+ """Parses `packages.find` configuration file section.
+
+ To be used in conjunction with _parse_packages().
+
+ :param dict section_options:
+ """
+ section_data = self._parse_section_to_dict(
+ section_options, self._parse_list)
+
+ valid_keys = ['where', 'include', 'exclude']
+
+ find_kwargs = dict(
+ [(k, v) for k, v in section_data.items() if k in valid_keys and v])
+
+ where = find_kwargs.get('where')
+ if where is not None:
+ find_kwargs['where'] = where[0] # cast list to single val
+
+ return find_kwargs
def parse_section_entry_points(self, section_options):
"""Parses `entry_points` configuration file section.
diff --git a/setuptools/dep_util.py b/setuptools/dep_util.py
new file mode 100644
index 00000000..2931c13e
--- /dev/null
+++ b/setuptools/dep_util.py
@@ -0,0 +1,23 @@
+from distutils.dep_util import newer_group
+
+# yes, this is was almost entirely copy-pasted from
+# 'newer_pairwise()', this is just another convenience
+# function.
+def newer_pairwise_group(sources_groups, targets):
+ """Walk both arguments in parallel, testing if each source group is newer
+ than its corresponding target. Returns a pair of lists (sources_groups,
+ targets) where sources is newer than target, according to the semantics
+ of 'newer_group()'.
+ """
+ if len(sources_groups) != len(targets):
+ raise ValueError("'sources_group' and 'targets' must be the same length")
+
+ # build a pair of lists (sources_groups, targets) where source is newer
+ n_sources = []
+ n_targets = []
+ for i in range(len(sources_groups)):
+ if newer_group(sources_groups[i], targets[i]):
+ n_sources.append(sources_groups[i])
+ n_targets.append(targets[i])
+
+ return n_sources, n_targets
diff --git a/setuptools/depends.py b/setuptools/depends.py
index 89d39a50..45e7052d 100644
--- a/setuptools/depends.py
+++ b/setuptools/depends.py
@@ -4,7 +4,8 @@ import marshal
from distutils.version import StrictVersion
from imp import PKG_DIRECTORY, PY_COMPILED, PY_SOURCE, PY_FROZEN
-from setuptools.extern import six
+from .py33compat import Bytecode
+
__all__ = [
'Require', 'find_module', 'get_module_constant', 'extract_constant'
@@ -78,39 +79,6 @@ class Require:
return self.version_ok(version)
-def _iter_code(code):
- """Yield '(op,arg)' pair for each operation in code object 'code'"""
-
- from array import array
- from dis import HAVE_ARGUMENT, EXTENDED_ARG
-
- bytes = array('b', code.co_code)
- eof = len(code.co_code)
-
- ptr = 0
- extended_arg = 0
-
- while ptr < eof:
-
- op = bytes[ptr]
-
- if op >= HAVE_ARGUMENT:
-
- arg = bytes[ptr + 1] + bytes[ptr + 2] * 256 + extended_arg
- ptr += 3
-
- if op == EXTENDED_ARG:
- long_type = six.integer_types[-1]
- extended_arg = arg * long_type(65536)
- continue
-
- else:
- arg = None
- ptr += 1
-
- yield op, arg
-
-
def find_module(module, paths=None):
"""Just like 'imp.find_module()', but with package support"""
@@ -176,9 +144,8 @@ def extract_constant(code, symbol, default=-1):
only 'STORE_NAME' and 'STORE_GLOBAL' opcodes are checked, and 'symbol'
must be present in 'code.co_names'.
"""
-
if symbol not in code.co_names:
- # name's not there, can't possibly be an assigment
+ # name's not there, can't possibly be an assignment
return None
name_idx = list(code.co_names).index(symbol)
@@ -189,7 +156,9 @@ def extract_constant(code, symbol, default=-1):
const = default
- for op, arg in _iter_code(code):
+ for byte_code in Bytecode(code):
+ op = byte_code.opcode
+ arg = byte_code.arg
if op == LOAD_CONST:
const = code.co_consts[arg]
diff --git a/setuptools/dist.py b/setuptools/dist.py
index c04e6426..a2ca8795 100644
--- a/setuptools/dist.py
+++ b/setuptools/dist.py
@@ -8,12 +8,15 @@ import distutils.log
import distutils.core
import distutils.cmd
import distutils.dist
-from distutils.errors import (DistutilsOptionError, DistutilsPlatformError,
- DistutilsSetupError)
+import itertools
+from collections import defaultdict
+from distutils.errors import (
+ DistutilsOptionError, DistutilsPlatformError, DistutilsSetupError,
+)
from distutils.util import rfc822_escape
from setuptools.extern import six
-from setuptools.extern.six.moves import map
+from setuptools.extern.six.moves import map, filter, filterfalse
from pkg_resources.extern import packaging
from setuptools.depends import Require
@@ -21,6 +24,10 @@ from setuptools import windows_support
from setuptools.monkey import get_unpatched
from setuptools.config import parse_configuration
import pkg_resources
+from .py36compat import Distribution_parse_config_files
+
+__import__('pkg_resources.extern.packaging.specifiers')
+__import__('pkg_resources.extern.packaging.version')
def _get_unpatched(cls):
@@ -51,6 +58,13 @@ def write_pkg_file(self, file):
if self.download_url:
file.write('Download-URL: %s\n' % self.download_url)
+ long_desc_content_type = getattr(
+ self,
+ 'long_description_content_type',
+ None
+ ) or 'UNKNOWN'
+ file.write('Description-Content-Type: %s\n' % long_desc_content_type)
+
long_desc = rfc822_escape(self.get_long_description())
file.write('Description: %s\n' % long_desc)
@@ -125,12 +139,7 @@ def check_nsp(dist, attr, value):
def check_extras(dist, attr, value):
"""Verify that extras_require mapping is valid"""
try:
- for k, v in value.items():
- if ':' in k:
- k, m = k.split(':', 1)
- if pkg_resources.invalid_marker(m):
- raise DistutilsSetupError("Invalid environment marker: " + m)
- list(pkg_resources.parse_requirements(v))
+ list(itertools.starmap(_check_extra, value.items()))
except (TypeError, ValueError, AttributeError):
raise DistutilsSetupError(
"'extras_require' must be a dictionary whose values are "
@@ -139,6 +148,13 @@ def check_extras(dist, attr, value):
)
+def _check_extra(extra, reqs):
+ name, sep, marker = extra.partition(':')
+ if marker and pkg_resources.invalid_marker(marker):
+ raise DistutilsSetupError("Invalid environment marker: " + marker)
+ list(pkg_resources.parse_requirements(reqs))
+
+
def assert_bool(dist, attr, value):
"""Verify that value is True, False, 0, or 1"""
if bool(value) != value:
@@ -164,7 +180,7 @@ def check_specifier(dist, attr, value):
packaging.specifiers.SpecifierSet(value)
except packaging.specifiers.InvalidSpecifier as error:
tmpl = (
- "{attr!r} must be a string or list of strings "
+ "{attr!r} must be a string "
"containing valid version specifiers; {error}"
)
raise DistutilsSetupError(tmpl.format(attr=attr, error=error))
@@ -213,7 +229,7 @@ def check_packages(dist, attr, value):
_Distribution = get_unpatched(distutils.core.Distribution)
-class Distribution(_Distribution):
+class Distribution(Distribution_parse_config_files, _Distribution):
"""Distribution with support for features, tests, and package data
This is an enhanced version of 'distutils.dist.Distribution' that
@@ -308,6 +324,9 @@ class Distribution(_Distribution):
self.dist_files = []
self.src_root = attrs and attrs.pop("src_root", None)
self.patch_missing_pkg_info(attrs)
+ self.long_description_content_type = _attrs_dict.get(
+ 'long_description_content_type'
+ )
# Make sure we have any eggs needed to interpret 'attrs'
if attrs is not None:
self.dependency_links = attrs.pop('dependency_links', [])
@@ -340,8 +359,73 @@ class Distribution(_Distribution):
"setuptools, pip, and PyPI. Please see PEP 440 for more "
"details." % self.metadata.version
)
+ self._finalize_requires()
+
+ def _finalize_requires(self):
+ """
+ Set `metadata.python_requires` and fix environment markers
+ in `install_requires` and `extras_require`.
+ """
if getattr(self, 'python_requires', None):
self.metadata.python_requires = self.python_requires
+ self._convert_extras_requirements()
+ self._move_install_requirements_markers()
+
+ def _convert_extras_requirements(self):
+ """
+ Convert requirements in `extras_require` of the form
+ `"extra": ["barbazquux; {marker}"]` to
+ `"extra:{marker}": ["barbazquux"]`.
+ """
+ spec_ext_reqs = getattr(self, 'extras_require', None) or {}
+ self._tmp_extras_require = defaultdict(list)
+ for section, v in spec_ext_reqs.items():
+ # Do not strip empty sections.
+ self._tmp_extras_require[section]
+ for r in pkg_resources.parse_requirements(v):
+ suffix = self._suffix_for(r)
+ self._tmp_extras_require[section + suffix].append(r)
+
+ @staticmethod
+ def _suffix_for(req):
+ """
+ For a requirement, return the 'extras_require' suffix for
+ that requirement.
+ """
+ return ':' + str(req.marker) if req.marker else ''
+
+ def _move_install_requirements_markers(self):
+ """
+ Move requirements in `install_requires` that are using environment
+ markers `extras_require`.
+ """
+
+ # divide the install_requires into two sets, simple ones still
+ # handled by install_requires and more complex ones handled
+ # by extras_require.
+
+ def is_simple_req(req):
+ return not req.marker
+
+ spec_inst_reqs = getattr(self, 'install_requires', None) or ()
+ inst_reqs = list(pkg_resources.parse_requirements(spec_inst_reqs))
+ simple_reqs = filter(is_simple_req, inst_reqs)
+ complex_reqs = filterfalse(is_simple_req, inst_reqs)
+ self.install_requires = list(map(str, simple_reqs))
+
+ for r in complex_reqs:
+ self._tmp_extras_require[':' + str(r.marker)].append(r)
+ self.extras_require = dict(
+ (k, [str(r) for r in map(self._clean_req, v)])
+ for k, v in self._tmp_extras_require.items()
+ )
+
+ def _clean_req(self, req):
+ """
+ Given a Requirement, remove environment markers and return it.
+ """
+ req.marker = None
+ return req
def parse_config_files(self, filenames=None):
"""Parses configuration files from various levels
@@ -351,6 +435,7 @@ class Distribution(_Distribution):
_Distribution.parse_config_files(self, filenames=filenames)
parse_configuration(self, self.command_options)
+ self._finalize_requires()
def parse_command_line(self):
"""Process features after parsing command line options"""
@@ -386,7 +471,10 @@ class Distribution(_Distribution):
ep.load()(self, ep.name, value)
if getattr(self, 'convert_2to3_doctests', None):
# XXX may convert to set here when we can rely on set being builtin
- self.convert_2to3_doctests = [os.path.abspath(p) for p in self.convert_2to3_doctests]
+ self.convert_2to3_doctests = [
+ os.path.abspath(p)
+ for p in self.convert_2to3_doctests
+ ]
else:
self.convert_2to3_doctests = []
@@ -407,35 +495,30 @@ class Distribution(_Distribution):
def fetch_build_egg(self, req):
"""Fetch an egg needed for building"""
-
- try:
- cmd = self._egg_fetcher
- cmd.package_index.to_scan = []
- except AttributeError:
- from setuptools.command.easy_install import easy_install
- dist = self.__class__({'script_args': ['easy_install']})
- dist.parse_config_files()
- opts = dist.get_option_dict('easy_install')
- keep = (
- 'find_links', 'site_dirs', 'index_url', 'optimize',
- 'site_dirs', 'allow_hosts'
- )
- for key in list(opts):
- if key not in keep:
- del opts[key] # don't use any other settings
- if self.dependency_links:
- links = self.dependency_links[:]
- if 'find_links' in opts:
- links = opts['find_links'][1].split() + links
- opts['find_links'] = ('setup', links)
- install_dir = self.get_egg_cache_dir()
- cmd = easy_install(
- dist, args=["x"], install_dir=install_dir, exclude_scripts=True,
- always_copy=False, build_directory=None, editable=False,
- upgrade=False, multi_version=True, no_report=True, user=False
- )
- cmd.ensure_finalized()
- self._egg_fetcher = cmd
+ from setuptools.command.easy_install import easy_install
+ dist = self.__class__({'script_args': ['easy_install']})
+ dist.parse_config_files()
+ opts = dist.get_option_dict('easy_install')
+ keep = (
+ 'find_links', 'site_dirs', 'index_url', 'optimize',
+ 'site_dirs', 'allow_hosts'
+ )
+ for key in list(opts):
+ if key not in keep:
+ del opts[key] # don't use any other settings
+ if self.dependency_links:
+ links = self.dependency_links[:]
+ if 'find_links' in opts:
+ links = opts['find_links'][1].split() + links
+ opts['find_links'] = ('setup', links)
+ install_dir = self.get_egg_cache_dir()
+ cmd = easy_install(
+ dist, args=["x"], install_dir=install_dir,
+ exclude_scripts=True,
+ always_copy=False, build_directory=None, editable=False,
+ upgrade=False, multi_version=True, no_report=True, user=False
+ )
+ cmd.ensure_finalized()
return cmd.easy_install(req)
def _set_global_opts_from_features(self):
@@ -455,8 +538,11 @@ class Distribution(_Distribution):
if not feature.include_by_default():
excdef, incdef = incdef, excdef
- go.append(('with-' + name, None, 'include ' + descr + incdef))
- go.append(('without-' + name, None, 'exclude ' + descr + excdef))
+ new = (
+ ('with-' + name, None, 'include ' + descr + incdef),
+ ('without-' + name, None, 'exclude ' + descr + excdef),
+ )
+ go.extend(new)
no['without-' + name] = 'with-' + name
self.global_options = self.feature_options = go + self.global_options
@@ -484,7 +570,8 @@ class Distribution(_Distribution):
if command in self.cmdclass:
return self.cmdclass[command]
- for ep in pkg_resources.iter_entry_points('distutils.commands', command):
+ eps = pkg_resources.iter_entry_points('distutils.commands', command)
+ for ep in eps:
ep.require(installer=self.fetch_build_egg)
self.cmdclass[command] = cmdclass = ep.load()
return cmdclass
@@ -618,7 +705,8 @@ class Distribution(_Distribution):
name + ": this setting cannot be changed via include/exclude"
)
else:
- setattr(self, name, old + [item for item in value if item not in old])
+ new = [item for item in value if item not in old]
+ setattr(self, name, old + new)
def exclude(self, **attrs):
"""Remove items from distribution that are named in keyword arguments
@@ -829,14 +917,14 @@ class Feature:
@staticmethod
def warn_deprecated():
- warnings.warn(
+ msg = (
"Features are deprecated and will be removed in a future "
- "version. See https://github.com/pypa/setuptools/issues/65.",
- DeprecationWarning,
- stacklevel=3,
+ "version. See https://github.com/pypa/setuptools/issues/65."
)
+ warnings.warn(msg, DeprecationWarning, stacklevel=3)
- def __init__(self, description, standard=False, available=True,
+ def __init__(
+ self, description, standard=False, available=True,
optional=True, require_features=(), remove=(), **extras):
self.warn_deprecated()
@@ -861,8 +949,8 @@ class Feature:
if not remove and not require_features and not extras:
raise DistutilsSetupError(
- "Feature %s: must define 'require_features', 'remove', or at least one"
- " of 'packages', 'py_modules', etc."
+ "Feature %s: must define 'require_features', 'remove', or "
+ "at least one of 'packages', 'py_modules', etc."
)
def include_by_default(self):
diff --git a/setuptools/monkey.py b/setuptools/monkey.py
index 09f208b1..d9eb7d7b 100644
--- a/setuptools/monkey.py
+++ b/setuptools/monkey.py
@@ -8,6 +8,7 @@ import platform
import types
import functools
from importlib import import_module
+import inspect
from setuptools.extern import six
@@ -20,6 +21,20 @@ if you think you need this functionality.
"""
+def _get_mro(cls):
+ """
+ Returns the bases classes for cls sorted by the MRO.
+
+ Works around an issue on Jython where inspect.getmro will not return all
+ base classes if multiple classes share the same name. Instead, this
+ function will return a tuple containing the class itself, and the contents
+ of cls.__bases__. See https://github.com/pypa/setuptools/issues/1024.
+ """
+ if platform.python_implementation() == "Jython":
+ return (cls,) + cls.__bases__
+ return inspect.getmro(cls)
+
+
def get_unpatched(item):
lookup = (
get_unpatched_class if isinstance(item, six.class_types) else
@@ -35,25 +50,23 @@ def get_unpatched_class(cls):
Also ensures that no other distutils extension monkeypatched the distutils
first.
"""
- while cls.__module__.startswith('setuptools'):
- cls, = cls.__bases__
- if not cls.__module__.startswith('distutils'):
+ external_bases = (
+ cls
+ for cls in _get_mro(cls)
+ if not cls.__module__.startswith('setuptools')
+ )
+ base = next(external_bases)
+ if not base.__module__.startswith('distutils'):
msg = "distutils has already been patched by %r" % cls
raise AssertionError(msg)
- return cls
+ return base
def patch_all():
# we can't patch distutils.cmd, alas
distutils.core.Command = setuptools.Command
- has_issue_12885 = (
- sys.version_info < (3, 4, 6)
- or
- (3, 5) < sys.version_info <= (3, 5, 3)
- or
- (3, 6) < sys.version_info
- )
+ has_issue_12885 = sys.version_info <= (3, 5, 3)
if has_issue_12885:
# fix findall bug in distutils (http://bugs.python.org/issue12885)
@@ -67,8 +80,6 @@ def patch_all():
(3, 4) < sys.version_info < (3, 4, 6)
or
(3, 5) < sys.version_info <= (3, 5, 3)
- or
- (3, 6) < sys.version_info
)
if needs_warehouse:
diff --git a/setuptools/msvc.py b/setuptools/msvc.py
index 447ddb38..8e3b638f 100644
--- a/setuptools/msvc.py
+++ b/setuptools/msvc.py
@@ -4,15 +4,17 @@ Improved support for Microsoft Visual C++ compilers.
Known supported compilers:
--------------------------
Microsoft Visual C++ 9.0:
- Microsoft Visual C++ Compiler for Python 2.7 (x86, amd64);
- Microsoft Windows SDK 7.0 (x86, x64, ia64);
+ Microsoft Visual C++ Compiler for Python 2.7 (x86, amd64)
Microsoft Windows SDK 6.1 (x86, x64, ia64)
+ Microsoft Windows SDK 7.0 (x86, x64, ia64)
Microsoft Visual C++ 10.0:
Microsoft Windows SDK 7.1 (x86, x64, ia64)
Microsoft Visual C++ 14.0:
Microsoft Visual C++ Build Tools 2015 (x86, x64, arm)
+ Microsoft Visual Studio 2017 (x86, x64, arm, arm64)
+ Microsoft Visual Studio Build Tools 2017 (x86, x64, arm, arm64)
"""
import os
@@ -43,9 +45,18 @@ else:
safe_env = dict()
+_msvc9_suppress_errors = (
+ # msvc9compiler isn't available on some platforms
+ ImportError,
+
+ # msvc9compiler raises DistutilsPlatformError in some
+ # environments. See #1118.
+ distutils.errors.DistutilsPlatformError,
+)
+
try:
from distutils.msvc9compiler import Reg
-except ImportError:
+except _msvc9_suppress_errors:
pass
@@ -94,7 +105,7 @@ def msvc9_find_vcvarsall(version):
def msvc9_query_vcvarsall(ver, arch='x86', *args, **kwargs):
"""
- Patched "distutils.msvc9compiler.query_vcvarsall" for support standalones
+ Patched "distutils.msvc9compiler.query_vcvarsall" for support extra
compilers.
Set environment without use of "vcvarsall.bat".
@@ -102,9 +113,9 @@ def msvc9_query_vcvarsall(ver, arch='x86', *args, **kwargs):
Known supported compilers
-------------------------
Microsoft Visual C++ 9.0:
- Microsoft Visual C++ Compiler for Python 2.7 (x86, amd64);
- Microsoft Windows SDK 7.0 (x86, x64, ia64);
+ Microsoft Visual C++ Compiler for Python 2.7 (x86, amd64)
Microsoft Windows SDK 6.1 (x86, x64, ia64)
+ Microsoft Windows SDK 7.0 (x86, x64, ia64)
Microsoft Visual C++ 10.0:
Microsoft Windows SDK 7.1 (x86, x64, ia64)
@@ -141,7 +152,7 @@ def msvc9_query_vcvarsall(ver, arch='x86', *args, **kwargs):
def msvc14_get_vc_env(plat_spec):
"""
- Patched "distutils._msvccompiler._get_vc_env" for support standalones
+ Patched "distutils._msvccompiler._get_vc_env" for support extra
compilers.
Set environment without use of "vcvarsall.bat".
@@ -150,6 +161,8 @@ def msvc14_get_vc_env(plat_spec):
-------------------------
Microsoft Visual C++ 14.0:
Microsoft Visual C++ Build Tools 2015 (x86, x64, arm)
+ Microsoft Visual Studio 2017 (x86, x64, arm, arm64)
+ Microsoft Visual Studio Build Tools 2017 (x86, x64, arm, arm64)
Parameters
----------
@@ -272,7 +285,7 @@ class PlatformInfo:
)
def target_dir(self, hidex86=False, x64=False):
- """
+ r"""
Target platform specific subfolder.
Parameters
@@ -294,7 +307,7 @@ class PlatformInfo:
)
def cross_dir(self, forcex86=False):
- """
+ r"""
Cross platform specific subfolder.
Parameters
@@ -411,7 +424,7 @@ class RegistryInfo:
------
str: value
"""
- node64 = '' if self.pi.current_is_x86() or x86 else r'\Wow6432Node'
+ node64 = '' if self.pi.current_is_x86() or x86 else 'Wow6432Node'
return os.path.join('Software', node64, 'Microsoft', key)
def lookup(self, key, name):
@@ -470,25 +483,26 @@ class SystemInfo:
def __init__(self, registry_info, vc_ver=None):
self.ri = registry_info
self.pi = self.ri.pi
- if vc_ver:
- self.vc_ver = vc_ver
- else:
- try:
- self.vc_ver = self.find_available_vc_vers()[-1]
- except IndexError:
- err = 'No Microsoft Visual C++ version found'
- raise distutils.errors.DistutilsPlatformError(err)
+ self.vc_ver = vc_ver or self._find_latest_available_vc_ver()
+
+ def _find_latest_available_vc_ver(self):
+ try:
+ return self.find_available_vc_vers()[-1]
+ except IndexError:
+ err = 'No Microsoft Visual C++ version found'
+ raise distutils.errors.DistutilsPlatformError(err)
def find_available_vc_vers(self):
"""
Find all available Microsoft Visual C++ versions.
"""
- vckeys = (self.ri.vc, self.ri.vc_for_python)
+ ms = self.ri.microsoft
+ vckeys = (self.ri.vc, self.ri.vc_for_python, self.ri.vs)
vc_vers = []
for hkey in self.ri.HKEYS:
for key in vckeys:
try:
- bkey = winreg.OpenKey(hkey, key, 0, winreg.KEY_READ)
+ bkey = winreg.OpenKey(hkey, ms(key), 0, winreg.KEY_READ)
except (OSError, IOError):
continue
subkeys, values, _ = winreg.QueryInfoKey(bkey)
@@ -525,9 +539,9 @@ class SystemInfo:
"""
Microsoft Visual C++ directory.
"""
- # Default path
- default = r'Microsoft Visual Studio %0.1f\VC' % self.vc_ver
- guess_vc = os.path.join(self.ProgramFilesx86, default)
+ self.VSInstallDir
+
+ guess_vc = self._guess_vc() or self._guess_vc_legacy()
# Try to get "VC++ for Python" path from registry as default path
reg_path = os.path.join(self.ri.vc_for_python, '%0.1f' % self.vc_ver)
@@ -543,12 +557,34 @@ class SystemInfo:
return path
+ def _guess_vc(self):
+ """
+ Locate Visual C for 2017
+ """
+ if self.vc_ver <= 14.0:
+ return
+
+ default = r'VC\Tools\MSVC'
+ guess_vc = os.path.join(self.VSInstallDir, default)
+ # Subdir with VC exact version as name
+ try:
+ vc_exact_ver = os.listdir(guess_vc)[-1]
+ return os.path.join(guess_vc, vc_exact_ver)
+ except (OSError, IOError, IndexError):
+ pass
+
+ def _guess_vc_legacy(self):
+ """
+ Locate Visual C for versions prior to 2017
+ """
+ default = r'Microsoft Visual Studio %0.1f\VC' % self.vc_ver
+ return os.path.join(self.ProgramFilesx86, default)
+
@property
def WindowsSdkVersion(self):
"""
- Microsoft Windows SDK versions.
+ Microsoft Windows SDK versions for specified MSVC++ version.
"""
- # Set Windows SDK versions for specified MSVC++ version
if self.vc_ver <= 9.0:
return ('7.0', '6.1', '6.0a')
elif self.vc_ver == 10.0:
@@ -561,6 +597,14 @@ class SystemInfo:
return ('10.0', '8.1')
@property
+ def WindowsSdkLastVersion(self):
+ """
+ Microsoft Windows SDK last version
+ """
+ return self._use_last_dir_name(os.path.join(
+ self.WindowsSdkDir, 'lib'))
+
+ @property
def WindowsSdkDir(self):
"""
Microsoft Windows SDK directory.
@@ -658,6 +702,14 @@ class SystemInfo:
return sdkdir or ''
@property
+ def UniversalCRTSdkLastVersion(self):
+ """
+ Microsoft Universal C Runtime SDK last version
+ """
+ return self._use_last_dir_name(os.path.join(
+ self.UniversalCRTSdkDir, 'lib'))
+
+ @property
def NetFxSdkVersion(self):
"""
Microsoft .NET Framework SDK versions.
@@ -716,7 +768,7 @@ class SystemInfo:
"""
return self._find_dot_net_versions(64)
- def _find_dot_net_versions(self, bits=32):
+ def _find_dot_net_versions(self, bits):
"""
Find Microsoft .NET Framework versions.
@@ -725,8 +777,10 @@ class SystemInfo:
bits: int
Platform number of bits: 32 or 64.
"""
- # Find actual .NET version
- ver = self.ri.lookup(self.ri.vc, 'frameworkver%d' % bits) or ''
+ # Find actual .NET version in registry
+ reg_ver = self.ri.lookup(self.ri.vc, 'frameworkver%d' % bits)
+ dot_net_dir = getattr(self, 'FrameworkDir%d' % bits)
+ ver = reg_ver or self._use_last_dir_name(dot_net_dir, 'v') or ''
# Set .NET versions for specified MSVC++ version
if self.vc_ver >= 12.0:
@@ -740,6 +794,25 @@ class SystemInfo:
frameworkver = ('v3.0', 'v2.0.50727')
return frameworkver
+ def _use_last_dir_name(self, path, prefix=''):
+ """
+ Return name of the last dir in path or '' if no dir found.
+
+ Parameters
+ ----------
+ path: str
+ Use dirs in this path
+ prefix: str
+ Use only dirs startings by this prefix
+ """
+ matching_dirs = (
+ dir_name
+ for dir_name in reversed(os.listdir(path))
+ if os.path.isdir(os.path.join(path, dir_name)) and
+ dir_name.startswith(prefix)
+ )
+ return next(matching_dirs, None) or ''
+
class EnvironmentInfo:
"""
@@ -765,15 +838,14 @@ class EnvironmentInfo:
# Variables and properties in this class use originals CamelCase variables
# names from Microsoft source files for more easy comparaison.
- def __init__(self, arch, vc_ver=None, vc_min_ver=None):
+ def __init__(self, arch, vc_ver=None, vc_min_ver=0):
self.pi = PlatformInfo(arch)
self.ri = RegistryInfo(self.pi)
self.si = SystemInfo(self.ri, vc_ver)
- if vc_min_ver:
- if self.vc_ver < vc_min_ver:
- err = 'No suitable Microsoft Visual C++ version found'
- raise distutils.errors.DistutilsPlatformError(err)
+ if self.vc_ver < vc_min_ver:
+ err = 'No suitable Microsoft Visual C++ version found'
+ raise distutils.errors.DistutilsPlatformError(err)
@property
def vc_ver(self):
@@ -810,7 +882,10 @@ class EnvironmentInfo:
"""
Microsoft Visual C++ & Microsoft Foundation Class Libraries
"""
- arch_subdir = self.pi.target_dir(hidex86=True)
+ if self.vc_ver >= 15.0:
+ arch_subdir = self.pi.target_dir(x64=True)
+ else:
+ arch_subdir = self.pi.target_dir(hidex86=True)
paths = ['Lib%s' % arch_subdir, r'ATLMFC\Lib%s' % arch_subdir]
if self.vc_ver >= 14.0:
@@ -840,10 +915,20 @@ class EnvironmentInfo:
if arch_subdir:
tools += [os.path.join(si.VCInstallDir, 'Bin%s' % arch_subdir)]
- if self.vc_ver >= 14.0:
+ if self.vc_ver == 14.0:
path = 'Bin%s' % self.pi.current_dir(hidex86=True)
tools += [os.path.join(si.VCInstallDir, path)]
+ elif self.vc_ver >= 15.0:
+ host_dir = (r'bin\HostX86%s' if self.pi.current_is_x86() else
+ r'bin\HostX64%s')
+ tools += [os.path.join(
+ si.VCInstallDir, host_dir % self.pi.target_dir(x64=True))]
+
+ if self.pi.current_cpu != self.pi.target_cpu:
+ tools += [os.path.join(
+ si.VCInstallDir, host_dir % self.pi.current_dir(x64=True))]
+
else:
tools += [os.path.join(si.VCInstallDir, 'Bin')]
@@ -861,8 +946,8 @@ class EnvironmentInfo:
else:
arch_subdir = self.pi.target_dir(x64=True)
lib = os.path.join(self.si.WindowsSdkDir, 'lib')
- libver = self._get_content_dirname(lib)
- return [os.path.join(lib, '%sum%s' % (libver, arch_subdir))]
+ libver = self._sdk_subdir
+ return [os.path.join(lib, '%sum%s' % (libver , arch_subdir))]
@property
def OSIncludes(self):
@@ -876,7 +961,7 @@ class EnvironmentInfo:
else:
if self.vc_ver >= 14.0:
- sdkver = self._get_content_dirname(include)
+ sdkver = self._sdk_subdir
else:
sdkver = ''
return [os.path.join(include, '%sshared' % sdkver),
@@ -933,13 +1018,20 @@ class EnvironmentInfo:
"""
Microsoft Windows SDK Tools
"""
- bin_dir = 'Bin' if self.vc_ver <= 11.0 else r'Bin\x86'
- tools = [os.path.join(self.si.WindowsSdkDir, bin_dir)]
+ return list(self._sdk_tools())
+
+ def _sdk_tools(self):
+ """
+ Microsoft Windows SDK Tools paths generator
+ """
+ if self.vc_ver < 15.0:
+ bin_dir = 'Bin' if self.vc_ver <= 11.0 else r'Bin\x86'
+ yield os.path.join(self.si.WindowsSdkDir, bin_dir)
if not self.pi.current_is_x86():
arch_subdir = self.pi.current_dir(x64=True)
path = 'Bin%s' % arch_subdir
- tools += [os.path.join(self.si.WindowsSdkDir, path)]
+ yield os.path.join(self.si.WindowsSdkDir, path)
if self.vc_ver == 10.0 or self.vc_ver == 11.0:
if self.pi.target_is_x86():
@@ -947,12 +1039,24 @@ class EnvironmentInfo:
else:
arch_subdir = self.pi.current_dir(hidex86=True, x64=True)
path = r'Bin\NETFX 4.0 Tools%s' % arch_subdir
- tools += [os.path.join(self.si.WindowsSdkDir, path)]
+ yield os.path.join(self.si.WindowsSdkDir, path)
+
+ elif self.vc_ver >= 15.0:
+ path = os.path.join(self.si.WindowsSdkDir, 'Bin')
+ arch_subdir = self.pi.current_dir(x64=True)
+ sdkver = self.si.WindowsSdkLastVersion
+ yield os.path.join(path, '%s%s' % (sdkver, arch_subdir))
if self.si.WindowsSDKExecutablePath:
- tools += [self.si.WindowsSDKExecutablePath]
+ yield self.si.WindowsSDKExecutablePath
- return tools
+ @property
+ def _sdk_subdir(self):
+ """
+ Microsoft Windows SDK version subdir
+ """
+ ucrtver = self.si.WindowsSdkLastVersion
+ return ('%s\\' % ucrtver) if ucrtver else ''
@property
def SdkSetup(self):
@@ -1023,10 +1127,21 @@ class EnvironmentInfo:
"""
if self.vc_ver < 12.0:
return []
+ elif self.vc_ver < 15.0:
+ base_path = self.si.ProgramFilesx86
+ arch_subdir = self.pi.current_dir(hidex86=True)
+ else:
+ base_path = self.si.VSInstallDir
+ arch_subdir = ''
- arch_subdir = self.pi.current_dir(hidex86=True)
path = r'MSBuild\%0.1f\bin%s' % (self.vc_ver, arch_subdir)
- return [os.path.join(self.si.ProgramFilesx86, path)]
+ build = [os.path.join(base_path, path)]
+
+ if self.vc_ver >= 15.0:
+ # Add Roslyn C# & Visual Basic Compiler
+ build += [os.path.join(base_path, path, 'Roslyn')]
+
+ return build
@property
def HTMLHelpWorkshop(self):
@@ -1041,27 +1156,34 @@ class EnvironmentInfo:
@property
def UCRTLibraries(self):
"""
- Microsoft Universal CRT Libraries
+ Microsoft Universal C Runtime SDK Libraries
"""
if self.vc_ver < 14.0:
return []
arch_subdir = self.pi.target_dir(x64=True)
lib = os.path.join(self.si.UniversalCRTSdkDir, 'lib')
- ucrtver = self._get_content_dirname(lib)
+ ucrtver = self._ucrt_subdir
return [os.path.join(lib, '%sucrt%s' % (ucrtver, arch_subdir))]
@property
def UCRTIncludes(self):
"""
- Microsoft Universal CRT Include
+ Microsoft Universal C Runtime SDK Include
"""
if self.vc_ver < 14.0:
return []
include = os.path.join(self.si.UniversalCRTSdkDir, 'include')
- ucrtver = self._get_content_dirname(include)
- return [os.path.join(include, '%sucrt' % ucrtver)]
+ return [os.path.join(include, '%sucrt' % self._ucrt_subdir)]
+
+ @property
+ def _ucrt_subdir(self):
+ """
+ Microsoft Universal C Runtime SDK version subdir
+ """
+ ucrtver = self.si.UniversalCRTSdkLastVersion
+ return ('%s\\' % ucrtver) if ucrtver else ''
@property
def FSharp(self):
@@ -1079,9 +1201,18 @@ class EnvironmentInfo:
Microsoft Visual C++ runtime redistribuable dll
"""
arch_subdir = self.pi.target_dir(x64=True)
- vcruntime = 'redist%s\\Microsoft.VC%d0.CRT\\vcruntime%d0.dll'
- vcruntime = vcruntime % (arch_subdir, self.vc_ver, self.vc_ver)
- return os.path.join(self.si.VCInstallDir, vcruntime)
+ if self.vc_ver < 15:
+ redist_path = self.si.VCInstallDir
+ vcruntime = 'redist%s\\Microsoft.VC%d0.CRT\\vcruntime%d0.dll'
+ else:
+ redist_path = self.si.VCInstallDir.replace('\\Tools', '\\Redist')
+ vcruntime = 'onecore%s\\Microsoft.VC%d0.CRT\\vcruntime%d0.dll'
+
+ # Visual Studio 2017 is still Visual C++ 14.0
+ dll_ver = 14.0 if self.vc_ver == 15 else self.vc_ver
+
+ vcruntime = vcruntime % (arch_subdir, self.vc_ver, dll_ver)
+ return os.path.join(redist_path, vcruntime)
def return_env(self, exists=True):
"""
@@ -1169,25 +1300,3 @@ class EnvironmentInfo:
if k not in seen:
seen_add(k)
yield element
-
- def _get_content_dirname(self, path):
- """
- Return name of the first dir in path or '' if no dir found.
-
- Parameters
- ----------
- path: str
- Path where search dir.
-
- Return
- ------
- foldername: str
- "name\" or ""
- """
- try:
- name = os.listdir(path)
- if name:
- return '%s\\' % name[0]
- return ''
- except (OSError, IOError):
- return ''
diff --git a/setuptools/namespaces.py b/setuptools/namespaces.py
index cc934b7e..dc16106d 100755
--- a/setuptools/namespaces.py
+++ b/setuptools/namespaces.py
@@ -30,15 +30,29 @@ class Installer:
with open(filename, 'wt') as f:
f.writelines(lines)
+ def uninstall_namespaces(self):
+ filename, ext = os.path.splitext(self._get_target())
+ filename += self.nspkg_ext
+ if not os.path.exists(filename):
+ return
+ log.info("Removing %s", filename)
+ os.remove(filename)
+
def _get_target(self):
return self.target
_nspkg_tmpl = (
"import sys, types, os",
- "pep420 = sys.version_info > (3, 3)",
+ "has_mfs = sys.version_info > (3, 5)",
"p = os.path.join(%(root)s, *%(pth)r)",
- "ie = os.path.exists(os.path.join(p,'__init__.py'))",
- "m = not ie and not pep420 and "
+ "importlib = has_mfs and __import__('importlib.util')",
+ "has_mfs and __import__('importlib.machinery')",
+ "m = has_mfs and "
+ "sys.modules.setdefault(%(pkg)r, "
+ "importlib.util.module_from_spec("
+ "importlib.machinery.PathFinder.find_spec(%(pkg)r, "
+ "[os.path.dirname(p)])))",
+ "m = m or "
"sys.modules.setdefault(%(pkg)r, types.ModuleType(%(pkg)r))",
"mp = (m or []) and m.__dict__.setdefault('__path__',[])",
"(p not in mp) and mp.append(p)",
diff --git a/setuptools/package_index.py b/setuptools/package_index.py
index d2f27ca6..4f610e0e 100755
--- a/setuptools/package_index.py
+++ b/setuptools/package_index.py
@@ -20,7 +20,7 @@ from setuptools.extern.six.moves import urllib, http_client, configparser, map
import setuptools
from pkg_resources import (
CHECKOUT_DIST, Distribution, BINARY_DIST, normalize_path, SOURCE_DIST,
- require, Environment, find_distributions, safe_name, safe_version,
+ Environment, find_distributions, safe_name, safe_version,
to_filename, Requirement, DEVELOP_DIST,
)
from setuptools import ssl_support
@@ -29,12 +29,12 @@ from distutils.errors import DistutilsError
from fnmatch import translate
from setuptools.py27compat import get_all_headers
-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)
# this is here to fix emacs' cruddy broken syntax highlighting
PYPI_MD5 = re.compile(
- '<a href="([^"#]+)">([^<]+)</a>\n\s+\\(<a (?:title="MD5 hash"\n\s+)'
- 'href="[^?]+\?:action=show_md5&amp;digest=([0-9a-f]{32})">md5</a>\\)'
+ '<a href="([^"#]+)">([^<]+)</a>\n\\s+\\(<a (?:title="MD5 hash"\n\\s+)'
+ 'href="[^?]+\\?:action=show_md5&amp;digest=([0-9a-f]{32})">md5</a>\\)'
)
URL_SCHEME = re.compile('([-+.a-z0-9]{2,}):', re.I).match
EXTENSIONS = ".tar.gz .tar.bz2 .tar .zip .tgz".split()
@@ -47,7 +47,7 @@ __all__ = [
_SOCKET_TIMEOUT = 15
_tmpl = "setuptools/{setuptools.__version__} Python-urllib/{py_major}"
-user_agent = _tmpl.format(py_major=sys.version[:3], **globals())
+user_agent = _tmpl.format(py_major=sys.version[:3], setuptools=setuptools)
def parse_requirement_arg(spec):
@@ -160,7 +160,7 @@ def interpret_distro_name(
# versions in distribution archive names (sdist and bdist).
parts = basename.split('-')
- if not py_version and any(re.match('py\d\.\d$', p) for p in parts[2:]):
+ if not py_version and any(re.match(r'py\d\.\d$', p) for p in parts[2:]):
# it is a bdist_dumb, not an sdist -- bail out
return
@@ -204,7 +204,7 @@ def unique_values(func):
return wrapper
-REL = re.compile("""<([^>]*\srel\s*=\s*['"]?([^'">]+)[^>]*)>""", re.I)
+REL = re.compile(r"""<([^>]*\srel\s*=\s*['"]?([^'">]+)[^>]*)>""", re.I)
# this line is here to fix emacs' cruddy broken syntax highlighting
diff --git a/setuptools/py27compat.py b/setuptools/py27compat.py
index 4e3e4ab3..2985011b 100644
--- a/setuptools/py27compat.py
+++ b/setuptools/py27compat.py
@@ -2,7 +2,9 @@
Compatibility Support for Python 2.7 and earlier
"""
-import sys
+import platform
+
+from setuptools.extern import six
def get_all_headers(message, key):
@@ -12,7 +14,15 @@ def get_all_headers(message, key):
return message.get_all(key)
-if sys.version_info < (3,):
-
+if six.PY2:
def get_all_headers(message, key):
return message.getheaders(key)
+
+
+linux_py2_ascii = (
+ platform.system() == 'Linux' and
+ six.PY2
+)
+
+rmtree_safe = str if linux_py2_ascii else lambda x: x
+"""Workaround for http://bugs.python.org/issue24672"""
diff --git a/setuptools/py33compat.py b/setuptools/py33compat.py
new file mode 100644
index 00000000..af64d5d1
--- /dev/null
+++ b/setuptools/py33compat.py
@@ -0,0 +1,45 @@
+import dis
+import array
+import collections
+
+from setuptools.extern import six
+
+
+OpArg = collections.namedtuple('OpArg', 'opcode arg')
+
+
+class Bytecode_compat(object):
+ def __init__(self, code):
+ self.code = code
+
+ def __iter__(self):
+ """Yield '(op,arg)' pair for each operation in code object 'code'"""
+
+ bytes = array.array('b', self.code.co_code)
+ eof = len(self.code.co_code)
+
+ ptr = 0
+ extended_arg = 0
+
+ while ptr < eof:
+
+ op = bytes[ptr]
+
+ if op >= dis.HAVE_ARGUMENT:
+
+ arg = bytes[ptr + 1] + bytes[ptr + 2] * 256 + extended_arg
+ ptr += 3
+
+ if op == dis.EXTENDED_ARG:
+ long_type = six.integer_types[-1]
+ extended_arg = arg * long_type(65536)
+ continue
+
+ else:
+ arg = None
+ ptr += 1
+
+ yield OpArg(op, arg)
+
+
+Bytecode = getattr(dis, 'Bytecode', Bytecode_compat)
diff --git a/setuptools/py36compat.py b/setuptools/py36compat.py
new file mode 100644
index 00000000..f5279696
--- /dev/null
+++ b/setuptools/py36compat.py
@@ -0,0 +1,82 @@
+import sys
+from distutils.errors import DistutilsOptionError
+from distutils.util import strtobool
+from distutils.debug import DEBUG
+
+
+class Distribution_parse_config_files:
+ """
+ Mix-in providing forward-compatibility for functionality to be
+ included by default on Python 3.7.
+
+ Do not edit the code in this class except to update functionality
+ as implemented in distutils.
+ """
+ def parse_config_files(self, filenames=None):
+ from configparser import ConfigParser
+
+ # Ignore install directory options if we have a venv
+ if sys.prefix != sys.base_prefix:
+ ignore_options = [
+ 'install-base', 'install-platbase', 'install-lib',
+ 'install-platlib', 'install-purelib', 'install-headers',
+ 'install-scripts', 'install-data', 'prefix', 'exec-prefix',
+ 'home', 'user', 'root']
+ else:
+ ignore_options = []
+
+ ignore_options = frozenset(ignore_options)
+
+ if filenames is None:
+ filenames = self.find_config_files()
+
+ if DEBUG:
+ self.announce("Distribution.parse_config_files():")
+
+ parser = ConfigParser(interpolation=None)
+ for filename in filenames:
+ if DEBUG:
+ self.announce(" reading %s" % filename)
+ parser.read(filename)
+ for section in parser.sections():
+ options = parser.options(section)
+ opt_dict = self.get_option_dict(section)
+
+ for opt in options:
+ if opt != '__name__' and opt not in ignore_options:
+ val = parser.get(section,opt)
+ opt = opt.replace('-', '_')
+ opt_dict[opt] = (filename, val)
+
+ # Make the ConfigParser forget everything (so we retain
+ # the original filenames that options come from)
+ parser.__init__()
+
+ # If there was a "global" section in the config file, use it
+ # to set Distribution options.
+
+ if 'global' in self.command_options:
+ for (opt, (src, val)) in self.command_options['global'].items():
+ alias = self.negative_opt.get(opt)
+ try:
+ if alias:
+ setattr(self, alias, not strtobool(val))
+ elif opt in ('verbose', 'dry_run'): # ugh!
+ setattr(self, opt, strtobool(val))
+ else:
+ setattr(self, opt, val)
+ except ValueError as msg:
+ raise DistutilsOptionError(msg)
+
+
+if sys.version_info < (3,):
+ # Python 2 behavior is sufficient
+ class Distribution_parse_config_files:
+ pass
+
+
+if False:
+ # When updated behavior is available upstream,
+ # disable override here.
+ class Distribution_parse_config_files:
+ pass
diff --git a/setuptools/sandbox.py b/setuptools/sandbox.py
index 640691d8..685f3f72 100755
--- a/setuptools/sandbox.py
+++ b/setuptools/sandbox.py
@@ -7,11 +7,12 @@ import itertools
import re
import contextlib
import pickle
+import textwrap
from setuptools.extern import six
from setuptools.extern.six.moves import builtins, map
-import pkg_resources
+import pkg_resources.py31compat
if sys.platform.startswith('java'):
import org.python.modules.posix.PosixModule as _os
@@ -25,6 +26,7 @@ _open = open
from distutils.errors import DistutilsError
from pkg_resources import working_set
+
__all__ = [
"AbstractSandbox", "DirectorySandbox", "SandboxViolation", "run_setup",
]
@@ -68,8 +70,7 @@ def override_temp(replacement):
"""
Monkey-patch tempfile.tempdir with replacement, ensuring it exists
"""
- if not os.path.isdir(replacement):
- os.makedirs(replacement)
+ pkg_resources.py31compat.makedirs(replacement, exist_ok=True)
saved = tempfile.tempdir
@@ -211,7 +212,7 @@ def _needs_hiding(mod_name):
>>> _needs_hiding('Cython')
True
"""
- pattern = re.compile('(setuptools|pkg_resources|distutils|Cython)(\.|$)')
+ pattern = re.compile(r'(setuptools|pkg_resources|distutils|Cython)(\.|$)')
return bool(pattern.match(mod_name))
@@ -237,11 +238,16 @@ def run_setup(setup_script, args):
working_set.__init__()
working_set.callbacks.append(lambda dist: dist.activate())
- def runner():
- ns = dict(__file__=setup_script, __name__='__main__')
- _execfile(setup_script, ns)
+ # __file__ should be a byte string on Python 2 (#712)
+ dunder_file = (
+ setup_script
+ if isinstance(setup_script, str) else
+ setup_script.encode(sys.getfilesystemencoding())
+ )
- DirectorySandbox(setup_dir).run(runner)
+ with DirectorySandbox(setup_dir):
+ ns = dict(__file__=dunder_file, __name__='__main__')
+ _execfile(setup_script, ns)
except SystemExit as v:
if v.args and v.args[0]:
raise
@@ -263,21 +269,24 @@ class AbstractSandbox:
for name in self._attrs:
setattr(os, name, getattr(source, name))
+ def __enter__(self):
+ self._copy(self)
+ if _file:
+ builtins.file = self._file
+ builtins.open = self._open
+ self._active = True
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ self._active = False
+ if _file:
+ builtins.file = _file
+ builtins.open = _open
+ self._copy(_os)
+
def run(self, func):
"""Run 'func' under os sandboxing"""
- try:
- self._copy(self)
- if _file:
- builtins.file = self._file
- builtins.open = self._open
- self._active = True
+ with self:
return func()
- finally:
- self._active = False
- if _file:
- builtins.file = _file
- builtins.open = _open
- self._copy(_os)
def _mk_dual_path_wrapper(name):
original = getattr(_os, name)
@@ -380,7 +389,7 @@ class DirectorySandbox(AbstractSandbox):
_exception_patterns = [
# Allow lib2to3 to attempt to save a pickled grammar object (#121)
- '.*lib2to3.*\.pickle$',
+ r'.*lib2to3.*\.pickle$',
]
"exempt writing to paths that match the pattern"
@@ -465,16 +474,18 @@ WRITE_FLAGS = functools.reduce(
class SandboxViolation(DistutilsError):
"""A setup script attempted to modify the filesystem outside the sandbox"""
- def __str__(self):
- return """SandboxViolation: %s%r %s
-
-The package setup script has attempted to modify files on your system
-that are not within the EasyInstall build area, and has been aborted.
+ tmpl = textwrap.dedent("""
+ SandboxViolation: {cmd}{args!r} {kwargs}
-This package cannot be safely installed by EasyInstall, and may not
-support alternate installation locations even if you run its setup
-script by hand. Please inform the package's author and the EasyInstall
-maintainers to find out if a fix or workaround is available.""" % self.args
+ The package setup script has attempted to modify files on your system
+ that are not within the EasyInstall build area, and has been aborted.
+ This package cannot be safely installed by EasyInstall, and may not
+ support alternate installation locations even if you run its setup
+ script by hand. Please inform the package's author and the EasyInstall
+ maintainers to find out if a fix or workaround is available.
+ """).lstrip()
-#
+ def __str__(self):
+ cmd, args, kwargs = self.args
+ return self.tmpl.format(**locals())
diff --git a/setuptools/ssl_support.py b/setuptools/ssl_support.py
index 82f8870a..72b18ef2 100644
--- a/setuptools/ssl_support.py
+++ b/setuptools/ssl_support.py
@@ -2,10 +2,10 @@ import os
import socket
import atexit
import re
+import functools
-from setuptools.extern.six.moves import urllib, http_client, map
+from setuptools.extern.six.moves import urllib, http_client, map, filter
-import pkg_resources
from pkg_resources import ResolutionError, ExtractionError
try:
@@ -204,47 +204,52 @@ def opener_for(ca_bundle=None):
).open
-_wincerts = None
+# from jaraco.functools
+def once(func):
+ @functools.wraps(func)
+ def wrapper(*args, **kwargs):
+ if not hasattr(func, 'always_returns'):
+ func.always_returns = func(*args, **kwargs)
+ return func.always_returns
+ return wrapper
+@once
def get_win_certfile():
- global _wincerts
- if _wincerts is not None:
- return _wincerts.name
-
try:
- from wincertstore import CertFile
+ import wincertstore
except ImportError:
return None
- class MyCertFile(CertFile):
- def __init__(self, stores=(), certs=()):
- CertFile.__init__(self)
- for store in stores:
- self.addstore(store)
- self.addcerts(certs)
+ class CertFile(wincertstore.CertFile):
+ def __init__(self):
+ super(CertFile, self).__init__()
atexit.register(self.close)
def close(self):
try:
- super(MyCertFile, self).close()
+ super(CertFile, self).close()
except OSError:
pass
- _wincerts = MyCertFile(stores=['CA', 'ROOT'])
+ _wincerts = CertFile()
+ _wincerts.addstore('CA')
+ _wincerts.addstore('ROOT')
return _wincerts.name
def find_ca_bundle():
"""Return an existing CA bundle path, or None"""
- if os.name == 'nt':
- return get_win_certfile()
- else:
- for cert_path in cert_paths:
- if os.path.isfile(cert_path):
- return cert_path
+ extant_cert_paths = filter(os.path.isfile, cert_paths)
+ return (
+ get_win_certfile()
+ or next(extant_cert_paths, None)
+ or _certifi_where()
+ )
+
+
+def _certifi_where():
try:
- import certifi
- return certifi.where()
+ return __import__('certifi').where()
except (ImportError, ResolutionError, ExtractionError):
- return None
+ pass
diff --git a/setuptools/tests/__init__.py b/setuptools/tests/__init__.py
index dbf16201..8ae4402d 100644
--- a/setuptools/tests/__init__.py
+++ b/setuptools/tests/__init__.py
@@ -1,4 +1,5 @@
"""Tests for the 'setuptools' package"""
+import locale
import sys
import os
import distutils.core
@@ -16,8 +17,7 @@ import setuptools.depends as dep
from setuptools import Feature
from setuptools.depends import Require
-c_type = os.environ.get("LC_CTYPE", os.environ.get("LC_ALL"))
-is_ascii = c_type in ("C", "POSIX")
+is_ascii = locale.getpreferredencoding() == 'ANSI_X3.4-1968'
fail_on_ascii = pytest.mark.xfail(is_ascii, reason="Test fails in this locale")
diff --git a/setuptools/tests/files.py b/setuptools/tests/files.py
index 4364241b..98de9fc3 100644
--- a/setuptools/tests/files.py
+++ b/setuptools/tests/files.py
@@ -1,6 +1,9 @@
import os
+import pkg_resources.py31compat
+
+
def build_files(file_defs, prefix=""):
"""
Build a set of files/directories, as described by the file_defs dictionary.
@@ -24,8 +27,7 @@ def build_files(file_defs, prefix=""):
for name, contents in file_defs.items():
full_name = os.path.join(prefix, name)
if isinstance(contents, dict):
- if not os.path.exists(full_name):
- os.makedirs(full_name)
+ pkg_resources.py31compat.makedirs(full_name, exist_ok=True)
build_files(contents, prefix=full_name)
else:
with open(full_name, 'w') as f:
diff --git a/setuptools/tests/mod_with_constant.py b/setuptools/tests/mod_with_constant.py
new file mode 100644
index 00000000..ef755dd1
--- /dev/null
+++ b/setuptools/tests/mod_with_constant.py
@@ -0,0 +1 @@
+value = 'three, sir!'
diff --git a/setuptools/tests/test_bdist_egg.py b/setuptools/tests/test_bdist_egg.py
index 5aabf404..d24aa366 100644
--- a/setuptools/tests/test_bdist_egg.py
+++ b/setuptools/tests/test_bdist_egg.py
@@ -41,4 +41,4 @@ class Test:
# let's see if we got our egg link at the right place
[content] = os.listdir('dist')
- assert re.match('foo-0.0.0-py[23].\d.egg$', content)
+ assert re.match(r'foo-0.0.0-py[23].\d.egg$', content)
diff --git a/setuptools/tests/test_build_clib.py b/setuptools/tests/test_build_clib.py
new file mode 100644
index 00000000..aebcc350
--- /dev/null
+++ b/setuptools/tests/test_build_clib.py
@@ -0,0 +1,59 @@
+import pytest
+import os
+import shutil
+
+import mock
+from distutils.errors import DistutilsSetupError
+from setuptools.command.build_clib import build_clib
+from setuptools.dist import Distribution
+
+
+class TestBuildCLib:
+ @mock.patch(
+ 'setuptools.command.build_clib.newer_pairwise_group'
+ )
+ def test_build_libraries(self, mock_newer):
+ dist = Distribution()
+ cmd = build_clib(dist)
+
+ # this will be a long section, just making sure all
+ # exceptions are properly raised
+ libs = [('example', {'sources': 'broken.c'})]
+ with pytest.raises(DistutilsSetupError):
+ cmd.build_libraries(libs)
+
+ obj_deps = 'some_string'
+ libs = [('example', {'sources': ['source.c'], 'obj_deps': obj_deps})]
+ with pytest.raises(DistutilsSetupError):
+ cmd.build_libraries(libs)
+
+ obj_deps = {'': ''}
+ libs = [('example', {'sources': ['source.c'], 'obj_deps': obj_deps})]
+ with pytest.raises(DistutilsSetupError):
+ cmd.build_libraries(libs)
+
+ obj_deps = {'source.c': ''}
+ libs = [('example', {'sources': ['source.c'], 'obj_deps': obj_deps})]
+ with pytest.raises(DistutilsSetupError):
+ cmd.build_libraries(libs)
+
+ # with that out of the way, let's see if the crude dependency
+ # system works
+ cmd.compiler = mock.MagicMock(spec=cmd.compiler)
+ mock_newer.return_value = ([],[])
+
+ obj_deps = {'': ('global.h',), 'example.c': ('example.h',)}
+ libs = [('example', {'sources': ['example.c'] ,'obj_deps': obj_deps})]
+
+ cmd.build_libraries(libs)
+ assert [['example.c', 'global.h', 'example.h']] in mock_newer.call_args[0]
+ assert not cmd.compiler.compile.called
+ assert cmd.compiler.create_static_lib.call_count == 1
+
+ # reset the call numbers so we can test again
+ cmd.compiler.reset_mock()
+
+ mock_newer.return_value = '' # anything as long as it's not ([],[])
+ cmd.build_libraries(libs)
+ assert cmd.compiler.compile.call_count == 1
+ assert cmd.compiler.create_static_lib.call_count == 1
diff --git a/setuptools/tests/test_config.py b/setuptools/tests/test_config.py
index 21487720..cdfa5af4 100644
--- a/setuptools/tests/test_config.py
+++ b/setuptools/tests/test_config.py
@@ -9,6 +9,13 @@ class ErrConfigHandler(ConfigHandler):
"""Erroneous handler. Fails to implement required methods."""
+def make_package_dir(name, base_dir):
+ dir_package = base_dir.mkdir(name)
+ init_file = dir_package.join('__init__.py')
+ init_file.write('')
+ return dir_package, init_file
+
+
def fake_env(tmpdir, setup_cfg, setup_py=None):
if setup_py is None:
@@ -18,11 +25,12 @@ def fake_env(tmpdir, setup_cfg, setup_py=None):
)
tmpdir.join('setup.py').write(setup_py)
- tmpdir.join('setup.cfg').write(setup_cfg)
+ config = tmpdir.join('setup.cfg')
+ config.write(setup_cfg)
- package_name = 'fake_package'
- dir_package = tmpdir.mkdir(package_name)
- dir_package.join('__init__.py').write(
+ package_dir, init_file = make_package_dir('fake_package', tmpdir)
+
+ init_file.write(
'VERSION = (1, 2, 3)\n'
'\n'
'VERSION_MAJOR = 1'
@@ -31,6 +39,7 @@ def fake_env(tmpdir, setup_cfg, setup_py=None):
' return [3, 4, 5, "dev"]\n'
'\n'
)
+ return package_dir, config
@contextlib.contextmanager
@@ -55,7 +64,7 @@ def test_parsers_implemented():
class TestConfigurationReader:
def test_basic(self, tmpdir):
- fake_env(
+ _, config = fake_env(
tmpdir,
'[metadata]\n'
'version = 10.1.1\n'
@@ -64,7 +73,7 @@ class TestConfigurationReader:
'[options]\n'
'scripts = bin/a.py, bin/b.py\n'
)
- config_dict = read_configuration('%s' % tmpdir.join('setup.cfg'))
+ config_dict = read_configuration('%s' % config)
assert config_dict['metadata']['version'] == '10.1.1'
assert config_dict['metadata']['keywords'] == ['one', 'two']
assert config_dict['options']['scripts'] == ['bin/a.py', 'bin/b.py']
@@ -73,6 +82,24 @@ class TestConfigurationReader:
with pytest.raises(DistutilsFileError):
read_configuration('%s' % tmpdir.join('setup.cfg'))
+ def test_ignore_errors(self, tmpdir):
+ _, config = fake_env(
+ tmpdir,
+ '[metadata]\n'
+ 'version = attr: none.VERSION\n'
+ 'keywords = one, two\n'
+ )
+ with pytest.raises(ImportError):
+ read_configuration('%s' % config)
+
+ config_dict = read_configuration(
+ '%s' % config, ignore_option_errors=True)
+
+ assert config_dict['metadata']['keywords'] == ['one', 'two']
+ assert 'version' not in config_dict['metadata']
+
+ config.remove()
+
class TestMetadata:
@@ -112,6 +139,24 @@ class TestMetadata:
assert metadata.download_url == 'http://test.test.com/test/'
assert metadata.maintainer_email == 'test@test.com'
+ def test_file_mixed(self, tmpdir):
+
+ fake_env(
+ tmpdir,
+ '[metadata]\n'
+ 'long_description = file: README.rst, CHANGES.rst\n'
+ '\n'
+ )
+
+ tmpdir.join('README.rst').write('readme contents\nline2')
+ tmpdir.join('CHANGES.rst').write('changelog contents\nand stuff')
+
+ with get_dist(tmpdir) as dist:
+ assert dist.metadata.long_description == (
+ 'readme contents\nline2\n'
+ 'changelog contents\nand stuff'
+ )
+
def test_file_sandboxed(self, tmpdir):
fake_env(
@@ -172,7 +217,7 @@ class TestMetadata:
def test_version(self, tmpdir):
- fake_env(
+ _, config = fake_env(
tmpdir,
'[metadata]\n'
'version = attr: fake_package.VERSION\n'
@@ -180,14 +225,14 @@ class TestMetadata:
with get_dist(tmpdir) as dist:
assert dist.metadata.version == '1.2.3'
- tmpdir.join('setup.cfg').write(
+ config.write(
'[metadata]\n'
'version = attr: fake_package.get_version\n'
)
with get_dist(tmpdir) as dist:
assert dist.metadata.version == '3.4.5.dev'
- tmpdir.join('setup.cfg').write(
+ config.write(
'[metadata]\n'
'version = attr: fake_package.VERSION_MAJOR\n'
)
@@ -198,7 +243,7 @@ class TestMetadata:
subpack.join('__init__.py').write('')
subpack.join('submodule.py').write('VERSION = (2016, 11, 26)')
- tmpdir.join('setup.cfg').write(
+ config.write(
'[metadata]\n'
'version = attr: fake_package.subpackage.submodule.VERSION\n'
)
@@ -230,11 +275,12 @@ class TestMetadata:
def test_classifiers(self, tmpdir):
expected = set([
'Framework :: Django',
+ 'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.5',
])
# From file.
- fake_env(
+ _, config = fake_env(
tmpdir,
'[metadata]\n'
'classifiers = file: classifiers\n'
@@ -242,19 +288,21 @@ class TestMetadata:
tmpdir.join('classifiers').write(
'Framework :: Django\n'
+ 'Programming Language :: Python :: 3\n'
'Programming Language :: Python :: 3.5\n'
)
with get_dist(tmpdir) as dist:
assert set(dist.metadata.classifiers) == expected
- # From section.
- tmpdir.join('setup.cfg').write(
- '[metadata.classifiers]\n'
- 'Framework :: Django\n'
- 'Programming Language :: Python :: 3.5\n'
+ # From list notation
+ config.write(
+ '[metadata]\n'
+ 'classifiers =\n'
+ ' Framework :: Django\n'
+ ' Programming Language :: Python :: 3\n'
+ ' Programming Language :: Python :: 3.5\n'
)
-
with get_dist(tmpdir) as dist:
assert set(dist.metadata.classifiers) == expected
@@ -282,6 +330,8 @@ class TestOptions:
'setup_requires = docutils>=0.3; spack ==1.1, ==1.3; there\n'
'dependency_links = http://some.com/here/1, '
'http://some.com/there/2\n'
+ 'python_requires = >=1.0, !=2.8\n'
+ 'py_modules = module1, module2\n'
)
with get_dist(tmpdir) as dist:
assert dist.zip_safe
@@ -301,7 +351,7 @@ class TestOptions:
])
assert dist.install_requires == ([
'docutils>=0.3',
- 'pack ==1.1, ==1.3',
+ 'pack==1.1,==1.3',
'hey'
])
assert dist.setup_requires == ([
@@ -310,6 +360,8 @@ class TestOptions:
'there'
])
assert dist.tests_require == ['mock==0.7.2', 'pytest']
+ assert dist.python_requires == '>=1.0, !=2.8'
+ assert dist.py_modules == ['module1', 'module2']
def test_multiline(self, tmpdir):
fake_env(
@@ -369,7 +421,7 @@ class TestOptions:
])
assert dist.install_requires == ([
'docutils>=0.3',
- 'pack ==1.1, ==1.3',
+ 'pack==1.1,==1.3',
'hey'
])
assert dist.setup_requires == ([
@@ -421,6 +473,46 @@ class TestOptions:
with get_dist(tmpdir) as dist:
assert dist.packages == ['fake_package']
+ def test_find_directive(self, tmpdir):
+ dir_package, config = fake_env(
+ tmpdir,
+ '[options]\n'
+ 'packages = find:\n'
+ )
+
+ dir_sub_one, _ = make_package_dir('sub_one', dir_package)
+ dir_sub_two, _ = make_package_dir('sub_two', dir_package)
+
+ with get_dist(tmpdir) as dist:
+ assert set(dist.packages) == set([
+ 'fake_package', 'fake_package.sub_two', 'fake_package.sub_one'
+ ])
+
+ config.write(
+ '[options]\n'
+ 'packages = find:\n'
+ '\n'
+ '[options.packages.find]\n'
+ 'where = .\n'
+ 'include =\n'
+ ' fake_package.sub_one\n'
+ ' two\n'
+ )
+ with get_dist(tmpdir) as dist:
+ assert dist.packages == ['fake_package.sub_one']
+
+ config.write(
+ '[options]\n'
+ 'packages = find:\n'
+ '\n'
+ '[options.packages.find]\n'
+ 'exclude =\n'
+ ' fake_package.sub_one\n'
+ )
+ with get_dist(tmpdir) as dist:
+ assert set(dist.packages) == set(
+ ['fake_package', 'fake_package.sub_two'])
+
def test_extras_require(self, tmpdir):
fake_env(
tmpdir,
@@ -434,11 +526,11 @@ class TestOptions:
with get_dist(tmpdir) as dist:
assert dist.extras_require == {
'pdf': ['ReportLab>=1.2', 'RXP'],
- 'rest': ['docutils>=0.3', 'pack ==1.1, ==1.3']
+ 'rest': ['docutils>=0.3', 'pack==1.1,==1.3']
}
def test_entry_points(self, tmpdir):
- fake_env(
+ _, config = fake_env(
tmpdir,
'[options.entry_points]\n'
'group1 = point1 = pack.module:func, '
@@ -463,7 +555,7 @@ class TestOptions:
tmpdir.join('entry_points').write(expected)
# From file.
- tmpdir.join('setup.cfg').write(
+ config.write(
'[options]\n'
'entry_points = file: entry_points\n'
)
diff --git a/setuptools/tests/test_dep_util.py b/setuptools/tests/test_dep_util.py
new file mode 100644
index 00000000..e5027c10
--- /dev/null
+++ b/setuptools/tests/test_dep_util.py
@@ -0,0 +1,30 @@
+from setuptools.dep_util import newer_pairwise_group
+import os
+import pytest
+
+
+@pytest.fixture
+def groups_target(tmpdir):
+ """Sets up some older sources, a target and newer sources.
+ Returns a 3-tuple in this order.
+ """
+ creation_order = ['older.c', 'older.h', 'target.o', 'newer.c', 'newer.h']
+ mtime = 0
+
+ for i in range(len(creation_order)):
+ creation_order[i] = os.path.join(str(tmpdir), creation_order[i])
+ with open(creation_order[i], 'w'):
+ pass
+
+ # make sure modification times are sequential
+ os.utime(creation_order[i], (mtime, mtime))
+ mtime += 1
+
+ return creation_order[:2], creation_order[2], creation_order[3:]
+
+
+def test_newer_pairwise_group(groups_target):
+ older = newer_pairwise_group([groups_target[0]], [groups_target[1]])
+ newer = newer_pairwise_group([groups_target[2]], [groups_target[1]])
+ assert older == ([], [])
+ assert newer == ([groups_target[2]], [groups_target[1]])
diff --git a/setuptools/tests/test_depends.py b/setuptools/tests/test_depends.py
new file mode 100644
index 00000000..e0cfa880
--- /dev/null
+++ b/setuptools/tests/test_depends.py
@@ -0,0 +1,16 @@
+import sys
+
+from setuptools import depends
+
+
+class TestGetModuleConstant:
+
+ def test_basic(self):
+ """
+ Invoke get_module_constant on a module in
+ the test package.
+ """
+ mod_name = 'setuptools.tests.mod_with_constant'
+ val = depends.get_module_constant(mod_name, 'value')
+ assert val == 'three, sir!'
+ assert 'setuptools.tests.mod_with_constant' not in sys.modules
diff --git a/setuptools/tests/test_develop.py b/setuptools/tests/test_develop.py
index 4cf483f2..ad7cfa05 100644
--- a/setuptools/tests/test_develop.py
+++ b/setuptools/tests/test_develop.py
@@ -1,17 +1,23 @@
"""develop tests
"""
+
+from __future__ import absolute_import, unicode_literals
+
import os
import site
import sys
import io
+import subprocess
from setuptools.extern import six
+from setuptools.command import test
import pytest
from setuptools.command.develop import develop
from setuptools.dist import Distribution
from . import contexts
+from . import namespaces
SETUP_PY = """\
from setuptools import setup
@@ -114,3 +120,72 @@ class TestDevelop:
cmd.install_dir = tmpdir
cmd.run()
# assert '0.0' not in foocmd_text
+
+
+class TestResolver:
+ """
+ TODO: These tests were written with a minimal understanding
+ of what _resolve_setup_path is intending to do. Come up with
+ more meaningful cases that look like real-world scenarios.
+ """
+ def test_resolve_setup_path_cwd(self):
+ assert develop._resolve_setup_path('.', '.', '.') == '.'
+
+ def test_resolve_setup_path_one_dir(self):
+ assert develop._resolve_setup_path('pkgs', '.', 'pkgs') == '../'
+
+ def test_resolve_setup_path_one_dir_trailing_slash(self):
+ assert develop._resolve_setup_path('pkgs/', '.', 'pkgs') == '../'
+
+
+class TestNamespaces:
+
+ @staticmethod
+ def install_develop(src_dir, target):
+
+ develop_cmd = [
+ sys.executable,
+ 'setup.py',
+ 'develop',
+ '--install-dir', str(target),
+ ]
+ with src_dir.as_cwd():
+ with test.test.paths_on_pythonpath([str(target)]):
+ subprocess.check_call(develop_cmd)
+
+ @pytest.mark.skipif(bool(os.environ.get("APPVEYOR")),
+ reason="https://github.com/pypa/setuptools/issues/851")
+ def test_namespace_package_importable(self, tmpdir):
+ """
+ Installing two packages sharing the same namespace, one installed
+ naturally using pip or `--single-version-externally-managed`
+ and the other installed using `develop` should leave the namespace
+ in tact and both packages reachable by import.
+ """
+ pkg_A = namespaces.build_namespace_package(tmpdir, 'myns.pkgA')
+ pkg_B = namespaces.build_namespace_package(tmpdir, 'myns.pkgB')
+ target = tmpdir / 'packages'
+ # use pip to install to the target directory
+ install_cmd = [
+ 'pip',
+ 'install',
+ str(pkg_A),
+ '-t', str(target),
+ ]
+ subprocess.check_call(install_cmd)
+ self.install_develop(pkg_B, target)
+ namespaces.make_site_dir(target)
+ try_import = [
+ sys.executable,
+ '-c', 'import myns.pkgA; import myns.pkgB',
+ ]
+ with test.test.paths_on_pythonpath([str(target)]):
+ subprocess.check_call(try_import)
+
+ # additionally ensure that pkg_resources import works
+ pkg_resources_imp = [
+ sys.executable,
+ '-c', 'import pkg_resources',
+ ]
+ with test.test.paths_on_pythonpath([str(target)]):
+ subprocess.check_call(pkg_resources_imp)
diff --git a/setuptools/tests/test_dist.py b/setuptools/tests/test_dist.py
new file mode 100644
index 00000000..435ffec0
--- /dev/null
+++ b/setuptools/tests/test_dist.py
@@ -0,0 +1,46 @@
+from setuptools import Distribution
+from setuptools.extern.six.moves.urllib.request import pathname2url
+from setuptools.extern.six.moves.urllib_parse import urljoin
+
+from .textwrap import DALS
+from .test_easy_install import make_nspkg_sdist
+
+
+def test_dist_fetch_build_egg(tmpdir):
+ """
+ Check multiple calls to `Distribution.fetch_build_egg` work as expected.
+ """
+ index = tmpdir.mkdir('index')
+ index_url = urljoin('file://', pathname2url(str(index)))
+ def sdist_with_index(distname, version):
+ dist_dir = index.mkdir(distname)
+ dist_sdist = '%s-%s.tar.gz' % (distname, version)
+ make_nspkg_sdist(str(dist_dir.join(dist_sdist)), distname, version)
+ with dist_dir.join('index.html').open('w') as fp:
+ fp.write(DALS(
+ '''
+ <!DOCTYPE html><html><body>
+ <a href="{dist_sdist}" rel="internal">{dist_sdist}</a><br/>
+ </body></html>
+ '''
+ ).format(dist_sdist=dist_sdist))
+ sdist_with_index('barbazquux', '3.2.0')
+ sdist_with_index('barbazquux-runner', '2.11.1')
+ with tmpdir.join('setup.cfg').open('w') as fp:
+ fp.write(DALS(
+ '''
+ [easy_install]
+ index_url = {index_url}
+ '''
+ ).format(index_url=index_url))
+ reqs = '''
+ barbazquux-runner
+ barbazquux
+ '''.split()
+ with tmpdir.as_cwd():
+ dist = Distribution()
+ resolved_dists = [
+ dist.fetch_build_egg(r)
+ for r in reqs
+ ]
+ assert [dist.key for dist in resolved_dists if dist] == reqs
diff --git a/setuptools/tests/test_easy_install.py b/setuptools/tests/test_easy_install.py
index 6f9bc8e1..e4ed556f 100644
--- a/setuptools/tests/test_easy_install.py
+++ b/setuptools/tests/test_easy_install.py
@@ -14,15 +14,12 @@ import itertools
import distutils.errors
import io
import zipfile
+import mock
import time
from setuptools.extern.six.moves import urllib
import pytest
-try:
- from unittest import mock
-except ImportError:
- import mock
from setuptools import sandbox
from setuptools.sandbox import run_setup
@@ -33,6 +30,7 @@ from setuptools.dist import Distribution
from pkg_resources import normalize_path, working_set
from pkg_resources import Distribution as PRDistribution
import setuptools.tests.server
+from setuptools.tests import fail_on_ascii
import pkg_resources
from . import contexts
@@ -67,7 +65,7 @@ class TestEasyInstallTest:
def test_get_script_args(self):
header = ei.CommandSpec.best().from_environment().as_header()
- expected = header + DALS("""
+ expected = header + DALS(r"""
# EASY-INSTALL-ENTRY-SCRIPT: 'spec','console_scripts','name'
__requires__ = 'spec'
import re
@@ -168,10 +166,7 @@ class TestEasyInstallTest:
sdist_zip.close()
return str(sdist)
- @pytest.mark.xfail(reason="#709 and #710")
- # also
- #@pytest.mark.xfail(setuptools.tests.is_ascii,
- # reason="https://github.com/pypa/setuptools/issues/706")
+ @fail_on_ascii
def test_unicode_filename_in_sdist(self, sdist_unicode, tmpdir, monkeypatch):
"""
The install command should execute correctly even if
@@ -571,18 +566,6 @@ def create_setup_requires_package(path, distname='foobar', version='0.1',
return test_pkg
-def make_trivial_sdist(dist_path, setup_py):
- """Create a simple sdist tarball at dist_path, containing just a
- setup.py, the contents of which are provided by the setup_py string.
- """
-
- setup_py_file = tarfile.TarInfo(name='setup.py')
- setup_py_bytes = io.BytesIO(setup_py.encode('utf-8'))
- setup_py_file.size = len(setup_py_bytes.getvalue())
- with tarfile.open(dist_path, 'w:gz') as dist:
- dist.addfile(setup_py_file, fileobj=setup_py_bytes)
-
-
@pytest.mark.skipif(
sys.platform.startswith('java') and ei.is_sh(sys.executable),
reason="Test cannot run under java when executable is sh"
diff --git a/setuptools/tests/test_egg_info.py b/setuptools/tests/test_egg_info.py
index 7bf6b68a..4c04d298 100644
--- a/setuptools/tests/test_egg_info.py
+++ b/setuptools/tests/test_egg_info.py
@@ -1,3 +1,5 @@
+import sys
+import ast
import os
import glob
import re
@@ -81,9 +83,8 @@ class TestEggInfo(object):
assert '[egg_info]' in content
assert 'tag_build =' in content
assert 'tag_date = 0' in content
- assert 'tag_svn_revision = 0' in content
- expected_order = 'tag_build', 'tag_date', 'tag_svn_revision'
+ expected_order = 'tag_build', 'tag_date',
self._validate_content_order(content, expected_order)
@@ -109,7 +110,6 @@ class TestEggInfo(object):
[egg_info]
tag_build =
tag_date = 0
- tag_svn_revision = 0
"""),
})
dist = Distribution()
@@ -123,9 +123,8 @@ class TestEggInfo(object):
assert '[egg_info]' in content
assert 'tag_build =' in content
assert 'tag_date = 0' in content
- assert 'tag_svn_revision = 0' in content
- expected_order = 'tag_build', 'tag_date', 'tag_svn_revision'
+ expected_order = 'tag_build', 'tag_date',
self._validate_content_order(content, expected_order)
@@ -160,48 +159,262 @@ class TestEggInfo(object):
sources_txt = os.path.join(egg_info_dir, 'SOURCES.txt')
assert 'docs/usage.rst' in open(sources_txt).read().split('\n')
- def _setup_script_with_requires(self, requires_line):
- setup_script = DALS("""
+ def _setup_script_with_requires(self, requires, use_setup_cfg=False):
+ setup_script = DALS(
+ '''
from setuptools import setup
- setup(
- name='foo',
- %s
- zip_safe=False,
- )
- """ % requires_line)
- build_files({
- 'setup.py': setup_script,
- })
+ setup(name='foo', zip_safe=False, %s)
+ '''
+ ) % ('' if use_setup_cfg else requires)
+ setup_config = requires if use_setup_cfg else ''
+ build_files({'setup.py': setup_script,
+ 'setup.cfg': setup_config})
+
+ mismatch_marker = "python_version<'{this_ver}'".format(
+ this_ver=sys.version_info[0],
+ )
+ # Alternate equivalent syntax.
+ mismatch_marker_alternate = 'python_version < "{this_ver}"'.format(
+ this_ver=sys.version_info[0],
+ )
+ invalid_marker = "<=>++"
+
+ class RequiresTestHelper(object):
+
+ @staticmethod
+ def parametrize(*test_list, **format_dict):
+ idlist = []
+ argvalues = []
+ for test in test_list:
+ test_params = test.lstrip().split('\n\n', 3)
+ name_kwargs = test_params.pop(0).split('\n')
+ if len(name_kwargs) > 1:
+ install_cmd_kwargs = ast.literal_eval(name_kwargs[1].strip())
+ else:
+ install_cmd_kwargs = {}
+ name = name_kwargs[0].strip()
+ setup_py_requires, setup_cfg_requires, expected_requires = (
+ DALS(a).format(**format_dict) for a in test_params
+ )
+ for id_, requires, use_cfg in (
+ (name, setup_py_requires, False),
+ (name + '_in_setup_cfg', setup_cfg_requires, True),
+ ):
+ idlist.append(id_)
+ marks = ()
+ if requires.startswith('@xfail\n'):
+ requires = requires[7:]
+ marks = pytest.mark.xfail
+ argvalues.append(pytest.param(requires, use_cfg,
+ expected_requires,
+ install_cmd_kwargs,
+ marks=marks))
+ return pytest.mark.parametrize('requires,use_setup_cfg,'
+ 'expected_requires,install_cmd_kwargs',
+ argvalues, ids=idlist)
+
+ @RequiresTestHelper.parametrize(
+ # Format of a test:
+ #
+ # id
+ # install_cmd_kwargs [optional]
+ #
+ # requires block (when used in setup.py)
+ #
+ # requires block (when used in setup.cfg)
+ #
+ # expected contents of requires.txt
+
+ '''
+ install_requires_with_marker
+
+ install_requires=["barbazquux;{mismatch_marker}"],
+
+ [options]
+ install_requires =
+ barbazquux; {mismatch_marker}
+
+ [:{mismatch_marker_alternate}]
+ barbazquux
+ ''',
+
+ '''
+ install_requires_with_extra
+ {'cmd': ['egg_info']}
+
+ install_requires=["barbazquux [test]"],
+
+ [options]
+ install_requires =
+ barbazquux [test]
+
+ barbazquux[test]
+ ''',
+
+ '''
+ install_requires_with_extra_and_marker
+
+ install_requires=["barbazquux [test]; {mismatch_marker}"],
+
+ [options]
+ install_requires =
+ barbazquux [test]; {mismatch_marker}
+
+ [:{mismatch_marker_alternate}]
+ barbazquux[test]
+ ''',
+
+ '''
+ setup_requires_with_markers
+
+ setup_requires=["barbazquux;{mismatch_marker}"],
+
+ [options]
+ setup_requires =
+ barbazquux; {mismatch_marker}
+
+ ''',
+
+ '''
+ tests_require_with_markers
+ {'cmd': ['test'], 'output': "Ran 0 tests in"}
+
+ tests_require=["barbazquux;{mismatch_marker}"],
+
+ [options]
+ tests_require =
+ barbazquux; {mismatch_marker}
+
+ ''',
+
+ '''
+ extras_require_with_extra
+ {'cmd': ['egg_info']}
- def test_install_requires_with_markers(self, tmpdir_cwd, env):
- self._setup_script_with_requires(
- """install_requires=["barbazquux;python_version<'2'"],""")
- self._run_install_command(tmpdir_cwd, env)
- egg_info_dir = self._find_egg_info_files(env.paths['lib']).base
+ extras_require={{"extra": ["barbazquux [test]"]}},
+
+ [options.extras_require]
+ extra = barbazquux [test]
+
+ [extra]
+ barbazquux[test]
+ ''',
+
+ '''
+ extras_require_with_extra_and_marker_in_req
+
+ extras_require={{"extra": ["barbazquux [test]; {mismatch_marker}"]}},
+
+ [options.extras_require]
+ extra =
+ barbazquux [test]; {mismatch_marker}
+
+ [extra]
+
+ [extra:{mismatch_marker_alternate}]
+ barbazquux[test]
+ ''',
+
+ # FIXME: ConfigParser does not allow : in key names!
+ '''
+ extras_require_with_marker
+
+ extras_require={{":{mismatch_marker}": ["barbazquux"]}},
+
+ @xfail
+ [options.extras_require]
+ :{mismatch_marker} = barbazquux
+
+ [:{mismatch_marker}]
+ barbazquux
+ ''',
+
+ '''
+ extras_require_with_marker_in_req
+
+ extras_require={{"extra": ["barbazquux; {mismatch_marker}"]}},
+
+ [options.extras_require]
+ extra =
+ barbazquux; {mismatch_marker}
+
+ [extra]
+
+ [extra:{mismatch_marker_alternate}]
+ barbazquux
+ ''',
+
+ '''
+ extras_require_with_empty_section
+
+ extras_require={{"empty": []}},
+
+ [options.extras_require]
+ empty =
+
+ [empty]
+ ''',
+ # Format arguments.
+ invalid_marker=invalid_marker,
+ mismatch_marker=mismatch_marker,
+ mismatch_marker_alternate=mismatch_marker_alternate,
+ )
+ def test_requires(self, tmpdir_cwd, env,
+ requires, use_setup_cfg,
+ expected_requires, install_cmd_kwargs):
+ self._setup_script_with_requires(requires, use_setup_cfg)
+ self._run_install_command(tmpdir_cwd, env, **install_cmd_kwargs)
+ egg_info_dir = os.path.join('.', 'foo.egg-info')
requires_txt = os.path.join(egg_info_dir, 'requires.txt')
- assert "barbazquux;python_version<'2'" in open(
- requires_txt).read().split('\n')
+ if os.path.exists(requires_txt):
+ with open(requires_txt) as fp:
+ install_requires = fp.read()
+ else:
+ install_requires = ''
+ assert install_requires.lstrip() == expected_requires
assert glob.glob(os.path.join(env.paths['lib'], 'barbazquux*')) == []
- def test_setup_requires_with_markers(self, tmpdir_cwd, env):
- self._setup_script_with_requires(
- """setup_requires=["barbazquux;python_version<'2'"],""")
- self._run_install_command(tmpdir_cwd, env)
+ def test_extras_require_with_invalid_marker(self, tmpdir_cwd, env):
+ tmpl = 'extras_require={{":{marker}": ["barbazquux"]}},'
+ req = tmpl.format(marker=self.invalid_marker)
+ self._setup_script_with_requires(req)
+ with pytest.raises(AssertionError):
+ self._run_install_command(tmpdir_cwd, env)
assert glob.glob(os.path.join(env.paths['lib'], 'barbazquux*')) == []
- def test_tests_require_with_markers(self, tmpdir_cwd, env):
- self._setup_script_with_requires(
- """tests_require=["barbazquux;python_version<'2'"],""")
- self._run_install_command(
- tmpdir_cwd, env, cmd=['test'], output="Ran 0 tests in")
+ def test_extras_require_with_invalid_marker_in_req(self, tmpdir_cwd, env):
+ tmpl = 'extras_require={{"extra": ["barbazquux; {marker}"]}},'
+ req = tmpl.format(marker=self.invalid_marker)
+ self._setup_script_with_requires(req)
+ with pytest.raises(AssertionError):
+ self._run_install_command(tmpdir_cwd, env)
assert glob.glob(os.path.join(env.paths['lib'], 'barbazquux*')) == []
- def test_extra_requires_with_markers(self, tmpdir_cwd, env):
+ def test_long_description_content_type(self, tmpdir_cwd, env):
+ # Test that specifying a `long_description_content_type` keyword arg to
+ # the `setup` function results in writing a `Description-Content-Type`
+ # line to the `PKG-INFO` file in the `<distribution>.egg-info`
+ # directory.
+ # `Description-Content-Type` is described at
+ # https://github.com/pypa/python-packaging-user-guide/pull/258
+
self._setup_script_with_requires(
- """extra_requires={":python_version<'2'": ["barbazquux"]},""")
- self._run_install_command(tmpdir_cwd, env)
- assert glob.glob(os.path.join(env.paths['lib'], 'barbazquux*')) == []
+ """long_description_content_type='text/markdown',""")
+ environ = os.environ.copy().update(
+ HOME=env.paths['home'],
+ )
+ code, data = environment.run_setup_py(
+ cmd=['egg_info'],
+ pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
+ data_stream=1,
+ env=environ,
+ )
+ egg_info_dir = os.path.join('.', 'foo.egg-info')
+ with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file:
+ pkg_info_lines = pkginfo_file.read().split('\n')
+ expected_line = 'Description-Content-Type: text/markdown'
+ assert expected_line in pkg_info_lines
def test_python_requires_egg_info(self, tmpdir_cwd, env):
self._setup_script_with_requires(
diff --git a/setuptools/tests/test_integration.py b/setuptools/tests/test_integration.py
index 78fb0627..3a9a6c50 100644
--- a/setuptools/tests/test_integration.py
+++ b/setuptools/tests/test_integration.py
@@ -98,3 +98,68 @@ def test_pbr(install_context):
def test_python_novaclient(install_context):
_install_one('python-novaclient', install_context,
'novaclient', 'base.py')
+
+
+def test_pyuri(install_context):
+ """
+ Install the pyuri package (version 0.3.1 at the time of writing).
+
+ This is also a regression test for issue #1016.
+ """
+ _install_one('pyuri', install_context, 'pyuri', 'uri.py')
+
+ pyuri = install_context.installed_projects['pyuri']
+
+ # The package data should be installed.
+ assert os.path.exists(os.path.join(pyuri.location, 'pyuri', 'uri.regex'))
+
+
+import re
+import subprocess
+import functools
+import tarfile, zipfile
+
+
+build_deps = ['appdirs', 'packaging', 'pyparsing', 'six']
+@pytest.mark.parametrize("build_dep", build_deps)
+@pytest.mark.skipif(sys.version_info < (3, 6), reason='run only on late versions')
+def test_build_deps_on_distutils(request, tmpdir_factory, build_dep):
+ """
+ All setuptools build dependencies must build without
+ setuptools.
+ """
+ if 'pyparsing' in build_dep:
+ pytest.xfail(reason="Project imports setuptools unconditionally")
+ build_target = tmpdir_factory.mktemp('source')
+ build_dir = download_and_extract(request, build_dep, build_target)
+ install_target = tmpdir_factory.mktemp('target')
+ output = install(build_dir, install_target)
+ for line in output.splitlines():
+ match = re.search('Unknown distribution option: (.*)', line)
+ allowed_unknowns = [
+ 'test_suite',
+ 'tests_require',
+ 'install_requires',
+ ]
+ assert not match or match.group(1).strip('"\'') in allowed_unknowns
+
+
+def install(pkg_dir, install_dir):
+ with open(os.path.join(pkg_dir, 'setuptools.py'), 'w') as breaker:
+ breaker.write('raise ImportError()')
+ cmd = [sys.executable, 'setup.py', 'install', '--prefix', install_dir]
+ env = dict(os.environ, PYTHONPATH=pkg_dir)
+ output = subprocess.check_output(cmd, cwd=pkg_dir, env=env, stderr=subprocess.STDOUT)
+ return output.decode('utf-8')
+
+
+def download_and_extract(request, req, target):
+ cmd = [sys.executable, '-m', 'pip', 'download', '--no-deps',
+ '--no-binary', ':all:', req]
+ output = subprocess.check_output(cmd, encoding='utf-8')
+ filename = re.search('Saved (.*)', output).group(1)
+ request.addfinalizer(functools.partial(os.remove, filename))
+ opener = zipfile.ZipFile if filename.endswith('.zip') else tarfile.open
+ with opener(filename) as archive:
+ archive.extractall(target)
+ return os.path.join(target, os.listdir(target)[0])
diff --git a/setuptools/tests/test_manifest.py b/setuptools/tests/test_manifest.py
index 62b6d708..65eec7d9 100644
--- a/setuptools/tests/test_manifest.py
+++ b/setuptools/tests/test_manifest.py
@@ -6,9 +6,11 @@ import os
import shutil
import sys
import tempfile
+import itertools
from distutils import log
from distutils.errors import DistutilsTemplateError
+import pkg_resources.py31compat
from setuptools.command.egg_info import FileList, egg_info, translate_pattern
from setuptools.dist import Distribution
from setuptools.extern import six
@@ -65,32 +67,94 @@ default_files = frozenset(map(make_local_path, [
]))
-def get_pattern(glob):
- return translate_pattern(make_local_path(glob)).pattern
-
-
-def test_translated_pattern_test():
- l = make_local_path
- assert get_pattern('foo') == r'foo\Z(?ms)'
- assert get_pattern(l('foo/bar')) == l(r'foo\/bar\Z(?ms)')
+translate_specs = [
+ ('foo', ['foo'], ['bar', 'foobar']),
+ ('foo/bar', ['foo/bar'], ['foo/bar/baz', './foo/bar', 'foo']),
# Glob matching
- assert get_pattern('*.txt') == l(r'[^\/]*\.txt\Z(?ms)')
- assert get_pattern('dir/*.txt') == l(r'dir\/[^\/]*\.txt\Z(?ms)')
- assert get_pattern('*/*.py') == l(r'[^\/]*\/[^\/]*\.py\Z(?ms)')
- assert get_pattern('docs/page-?.txt') \
- == l(r'docs\/page\-[^\/]\.txt\Z(?ms)')
+ ('*.txt', ['foo.txt', 'bar.txt'], ['foo/foo.txt']),
+ ('dir/*.txt', ['dir/foo.txt', 'dir/bar.txt', 'dir/.txt'], ['notdir/foo.txt']),
+ ('*/*.py', ['bin/start.py'], []),
+ ('docs/page-?.txt', ['docs/page-9.txt'], ['docs/page-10.txt']),
# Globstars change what they mean depending upon where they are
- assert get_pattern(l('foo/**/bar')) == l(r'foo\/(?:[^\/]+\/)*bar\Z(?ms)')
- assert get_pattern(l('foo/**')) == l(r'foo\/.*\Z(?ms)')
- assert get_pattern(l('**')) == r'.*\Z(?ms)'
+ (
+ 'foo/**/bar',
+ ['foo/bing/bar', 'foo/bing/bang/bar', 'foo/bar'],
+ ['foo/abar'],
+ ),
+ (
+ 'foo/**',
+ ['foo/bar/bing.py', 'foo/x'],
+ ['/foo/x'],
+ ),
+ (
+ '**',
+ ['x', 'abc/xyz', '@nything'],
+ [],
+ ),
# Character classes
- assert get_pattern('pre[one]post') == r'pre[one]post\Z(?ms)'
- assert get_pattern('hello[!one]world') == r'hello[^one]world\Z(?ms)'
- assert get_pattern('[]one].txt') == r'[\]one]\.txt\Z(?ms)'
- assert get_pattern('foo[!]one]bar') == r'foo[^\]one]bar\Z(?ms)'
+ (
+ 'pre[one]post',
+ ['preopost', 'prenpost', 'preepost'],
+ ['prepost', 'preonepost'],
+ ),
+
+ (
+ 'hello[!one]world',
+ ['helloxworld', 'helloyworld'],
+ ['hellooworld', 'helloworld', 'hellooneworld'],
+ ),
+
+ (
+ '[]one].txt',
+ ['o.txt', '].txt', 'e.txt'],
+ ['one].txt'],
+ ),
+
+ (
+ 'foo[!]one]bar',
+ ['fooybar'],
+ ['foo]bar', 'fooobar', 'fooebar'],
+ ),
+
+]
+"""
+A spec of inputs for 'translate_pattern' and matches and mismatches
+for that input.
+"""
+
+match_params = itertools.chain.from_iterable(
+ zip(itertools.repeat(pattern), matches)
+ for pattern, matches, mismatches in translate_specs
+)
+
+
+@pytest.fixture(params=match_params)
+def pattern_match(request):
+ return map(make_local_path, request.param)
+
+
+mismatch_params = itertools.chain.from_iterable(
+ zip(itertools.repeat(pattern), mismatches)
+ for pattern, matches, mismatches in translate_specs
+)
+
+
+@pytest.fixture(params=mismatch_params)
+def pattern_mismatch(request):
+ return map(make_local_path, request.param)
+
+
+def test_translated_pattern_match(pattern_match):
+ pattern, target = pattern_match
+ assert translate_pattern(pattern).match(target)
+
+
+def test_translated_pattern_mismatch(pattern_mismatch):
+ pattern, target = pattern_mismatch
+ assert not translate_pattern(pattern).match(target)
class TempDirTestCase(object):
@@ -206,6 +270,15 @@ class TestManifestTest(TempDirTestCase):
l('app/static/app.css'), l('app/static/app.css.map')])
assert files == self.get_files()
+ def test_graft_glob_syntax(self):
+ """Include the whole app/static/ directory."""
+ l = make_local_path
+ self.make_manifest("graft */static")
+ files = default_files | set([
+ l('app/static/app.js'), l('app/static/app.js.map'),
+ l('app/static/app.css'), l('app/static/app.css.map')])
+ assert files == self.get_files()
+
def test_graft_global_exclude(self):
"""Exclude all *.map files in the project."""
l = make_local_path
@@ -289,8 +362,7 @@ class TestFileListTest(TempDirTestCase):
for file in files:
file = os.path.join(self.temp_dir, file)
dirname, basename = os.path.split(file)
- if not os.path.exists(dirname):
- os.makedirs(dirname)
+ pkg_resources.py31compat.makedirs(dirname, exist_ok=True)
open(file, 'w').close()
def test_process_template_line(self):
@@ -449,11 +521,6 @@ class TestFileListTest(TempDirTestCase):
assert file_list.files == ['a.py', l('d/c.py')]
self.assertWarnings()
- file_list.process_template_line('global-include .txt')
- file_list.sort()
- assert file_list.files == ['a.py', 'b.txt', l('d/c.py')]
- self.assertNoWarnings()
-
def test_global_exclude(self):
l = make_local_path
# global-exclude
@@ -470,13 +537,6 @@ class TestFileListTest(TempDirTestCase):
assert file_list.files == ['b.txt']
self.assertWarnings()
- file_list = FileList()
- file_list.files = ['a.py', 'b.txt', l('d/c.pyc'), 'e.pyo']
- file_list.process_template_line('global-exclude .py[co]')
- file_list.sort()
- assert file_list.files == ['a.py', 'b.txt']
- self.assertNoWarnings()
-
def test_recursive_include(self):
l = make_local_path
# recursive-include
diff --git a/setuptools/tests/test_msvc.py b/setuptools/tests/test_msvc.py
index a0c76ea0..32d7a907 100644
--- a/setuptools/tests/test_msvc.py
+++ b/setuptools/tests/test_msvc.py
@@ -5,12 +5,9 @@ Tests for msvc support module.
import os
import contextlib
import distutils.errors
+import mock
import pytest
-try:
- from unittest import mock
-except ImportError:
- import mock
from . import contexts
diff --git a/setuptools/tests/test_namespaces.py b/setuptools/tests/test_namespaces.py
index 28c5e9de..721cad1e 100644
--- a/setuptools/tests/test_namespaces.py
+++ b/setuptools/tests/test_namespaces.py
@@ -7,12 +7,13 @@ import subprocess
import pytest
from . import namespaces
+from setuptools.command import test
class TestNamespaces:
- @pytest.mark.xfail(sys.version_info < (3, 3),
- reason="Requires PEP 420")
+ @pytest.mark.xfail(sys.version_info < (3, 5),
+ reason="Requires importlib.util.module_from_spec")
@pytest.mark.skipif(bool(os.environ.get("APPVEYOR")),
reason="https://github.com/pypa/setuptools/issues/851")
def test_mixed_site_and_non_site(self, tmpdir):
@@ -27,7 +28,6 @@ class TestNamespaces:
site_packages = tmpdir / 'site-packages'
path_packages = tmpdir / 'path-packages'
targets = site_packages, path_packages
- python_path = os.pathsep.join(map(str, targets))
# use pip to install to the target directory
install_cmd = [
'pip',
@@ -48,5 +48,58 @@ class TestNamespaces:
sys.executable,
'-c', 'import myns.pkgA; import myns.pkgB',
]
- env = dict(PYTHONPATH=python_path)
- subprocess.check_call(try_import, env=env)
+ with test.test.paths_on_pythonpath(map(str, targets)):
+ subprocess.check_call(try_import)
+
+ @pytest.mark.skipif(bool(os.environ.get("APPVEYOR")),
+ reason="https://github.com/pypa/setuptools/issues/851")
+ def test_pkg_resources_import(self, tmpdir):
+ """
+ Ensure that a namespace package doesn't break on import
+ of pkg_resources.
+ """
+ pkg = namespaces.build_namespace_package(tmpdir, 'myns.pkgA')
+ target = tmpdir / 'packages'
+ target.mkdir()
+ install_cmd = [
+ sys.executable,
+ '-m', 'easy_install',
+ '-d', str(target),
+ str(pkg),
+ ]
+ with test.test.paths_on_pythonpath([str(target)]):
+ subprocess.check_call(install_cmd)
+ namespaces.make_site_dir(target)
+ try_import = [
+ sys.executable,
+ '-c', 'import pkg_resources',
+ ]
+ with test.test.paths_on_pythonpath([str(target)]):
+ subprocess.check_call(try_import)
+
+ @pytest.mark.skipif(bool(os.environ.get("APPVEYOR")),
+ reason="https://github.com/pypa/setuptools/issues/851")
+ def test_namespace_package_installed_and_cwd(self, tmpdir):
+ """
+ Installing a namespace packages but also having it in the current
+ working directory, only one version should take precedence.
+ """
+ pkg_A = namespaces.build_namespace_package(tmpdir, 'myns.pkgA')
+ target = tmpdir / 'packages'
+ # use pip to install to the target directory
+ install_cmd = [
+ 'pip',
+ 'install',
+ str(pkg_A),
+ '-t', str(target),
+ ]
+ subprocess.check_call(install_cmd)
+ namespaces.make_site_dir(target)
+
+ # ensure that package imports and pkg_resources imports
+ pkg_resources_imp = [
+ sys.executable,
+ '-c', 'import pkg_resources; import myns.pkgA',
+ ]
+ with test.test.paths_on_pythonpath([str(target)]):
+ subprocess.check_call(pkg_resources_imp, cwd=str(pkg_A))
diff --git a/setuptools/tests/test_packageindex.py b/setuptools/tests/test_packageindex.py
index f09dd78c..53e20d44 100644
--- a/setuptools/tests/test_packageindex.py
+++ b/setuptools/tests/test_packageindex.py
@@ -181,6 +181,48 @@ class TestPackageIndex:
res = setuptools.package_index.local_open(url)
assert 'content' in res.read()
+ def test_egg_fragment(self):
+ """
+ EGG fragments must comply to PEP 440
+ """
+ epoch = [
+ '',
+ '1!',
+ ]
+ releases = [
+ '0',
+ '0.0',
+ '0.0.0',
+ ]
+ pre = [
+ 'a0',
+ 'b0',
+ 'rc0',
+ ]
+ post = [
+ '.post0'
+ ]
+ dev = [
+ '.dev0',
+ ]
+ local = [
+ ('', ''),
+ ('+ubuntu.0', '+ubuntu.0'),
+ ('+ubuntu-0', '+ubuntu.0'),
+ ('+ubuntu_0', '+ubuntu.0'),
+ ]
+ versions = [
+ [''.join([e, r, p, l]) for l in ll]
+ for e in epoch
+ for r in releases
+ for p in sum([pre, post, dev], [''])
+ for ll in local]
+ for v, vc in versions:
+ dists = list(setuptools.package_index.distros_for_url(
+ 'http://example.com/example.zip#egg=example-' + v))
+ assert dists[0].version == ''
+ assert dists[1].version == vc
+
class TestContentCheckers:
def test_md5(self):
diff --git a/setuptools/tests/test_sandbox.py b/setuptools/tests/test_sandbox.py
index 929f0a5b..a3f1206d 100644
--- a/setuptools/tests/test_sandbox.py
+++ b/setuptools/tests/test_sandbox.py
@@ -7,13 +7,12 @@ import pytest
import pkg_resources
import setuptools.sandbox
-from setuptools.sandbox import DirectorySandbox
class TestSandbox:
def test_devnull(self, tmpdir):
- sandbox = DirectorySandbox(str(tmpdir))
- sandbox.run(self._file_writer(os.devnull))
+ with setuptools.sandbox.DirectorySandbox(str(tmpdir)):
+ self._file_writer(os.devnull)
@staticmethod
def _file_writer(path):
@@ -116,13 +115,17 @@ class TestExceptionSaver:
with open('/etc/foo', 'w'):
pass
- sandbox = DirectorySandbox(str(tmpdir))
with pytest.raises(setuptools.sandbox.SandboxViolation) as caught:
with setuptools.sandbox.save_modules():
setuptools.sandbox.hide_setuptools()
- sandbox.run(write_file)
+ with setuptools.sandbox.DirectorySandbox(str(tmpdir)):
+ write_file()
cmd, args, kwargs = caught.value.args
assert cmd == 'open'
assert args == ('/etc/foo', 'w')
assert kwargs == {}
+
+ msg = str(caught.value)
+ assert 'open' in msg
+ assert "('/etc/foo', 'w')" in msg
diff --git a/setuptools/tests/test_upload_docs.py b/setuptools/tests/test_upload_docs.py
index 5d50bb0b..a26e32a6 100644
--- a/setuptools/tests/test_upload_docs.py
+++ b/setuptools/tests/test_upload_docs.py
@@ -64,6 +64,8 @@ class TestUploadDocsTest:
)
body, content_type = upload_docs._build_multipart(data)
assert 'form-data' in content_type
+ assert "b'" not in content_type
+ assert 'b"' not in content_type
assert isinstance(body, bytes)
assert b'foo' in body
assert b'content' in body
diff --git a/setuptools/tests/test_virtualenv.py b/setuptools/tests/test_virtualenv.py
new file mode 100644
index 00000000..17b8793c
--- /dev/null
+++ b/setuptools/tests/test_virtualenv.py
@@ -0,0 +1,116 @@
+import glob
+import os
+
+from pytest import yield_fixture
+from pytest_fixture_config import yield_requires_config
+
+import pytest_virtualenv
+
+from .textwrap import DALS
+from .test_easy_install import make_nspkg_sdist
+
+
+@yield_requires_config(pytest_virtualenv.CONFIG, ['virtualenv_executable'])
+@yield_fixture(scope='function')
+def bare_virtualenv():
+ """ Bare virtualenv (no pip/setuptools/wheel).
+ """
+ with pytest_virtualenv.VirtualEnv(args=(
+ '--no-wheel',
+ '--no-pip',
+ '--no-setuptools',
+ )) as venv:
+ yield venv
+
+
+SOURCE_DIR = os.path.join(os.path.dirname(__file__), '../..')
+
+def test_clean_env_install(bare_virtualenv):
+ """
+ Check setuptools can be installed in a clean environment.
+ """
+ bare_virtualenv.run(' && '.join((
+ 'cd {source}',
+ 'python setup.py install',
+ )).format(source=SOURCE_DIR))
+
+def test_pip_upgrade_from_source(virtualenv):
+ """
+ Check pip can upgrade setuptools from source.
+ """
+ dist_dir = virtualenv.workspace
+ # Generate source distribution / wheel.
+ virtualenv.run(' && '.join((
+ 'cd {source}',
+ 'python setup.py -q sdist -d {dist}',
+ 'python setup.py -q bdist_wheel -d {dist}',
+ )).format(source=SOURCE_DIR, dist=dist_dir))
+ sdist = glob.glob(os.path.join(dist_dir, '*.zip'))[0]
+ wheel = glob.glob(os.path.join(dist_dir, '*.whl'))[0]
+ # Then update from wheel.
+ virtualenv.run('pip install ' + wheel)
+ # And finally try to upgrade from source.
+ virtualenv.run('pip install --no-cache-dir --upgrade ' + sdist)
+
+def test_test_command_install_requirements(bare_virtualenv, tmpdir):
+ """
+ Check the test command will install all required dependencies.
+ """
+ bare_virtualenv.run(' && '.join((
+ 'cd {source}',
+ 'python setup.py develop',
+ )).format(source=SOURCE_DIR))
+ def sdist(distname, version):
+ dist_path = tmpdir.join('%s-%s.tar.gz' % (distname, version))
+ make_nspkg_sdist(str(dist_path), distname, version)
+ return dist_path
+ dependency_links = [
+ str(dist_path)
+ for dist_path in (
+ sdist('foobar', '2.4'),
+ sdist('bits', '4.2'),
+ sdist('bobs', '6.0'),
+ sdist('pieces', '0.6'),
+ )
+ ]
+ with tmpdir.join('setup.py').open('w') as fp:
+ fp.write(DALS(
+ '''
+ from setuptools import setup
+
+ setup(
+ dependency_links={dependency_links!r},
+ install_requires=[
+ 'barbazquux1; sys_platform in ""',
+ 'foobar==2.4',
+ ],
+ setup_requires='bits==4.2',
+ tests_require="""
+ bobs==6.0
+ """,
+ extras_require={{
+ 'test': ['barbazquux2'],
+ ':"" in sys_platform': 'pieces==0.6',
+ ':python_version > "1"': """
+ pieces
+ foobar
+ """,
+ }}
+ )
+ '''.format(dependency_links=dependency_links)))
+ with tmpdir.join('test.py').open('w') as fp:
+ fp.write(DALS(
+ '''
+ import foobar
+ import bits
+ import bobs
+ import pieces
+
+ open('success', 'w').close()
+ '''))
+ # Run test command for test package.
+ bare_virtualenv.run(' && '.join((
+ 'cd {tmpdir}',
+ 'python setup.py test -s test',
+ )).format(tmpdir=tmpdir))
+ assert tmpdir.join('success').check()