diff options
| author | Jason R. Coombs <jaraco@jaraco.com> | 2017-11-19 21:39:41 -0500 |
|---|---|---|
| committer | Jason R. Coombs <jaraco@jaraco.com> | 2017-11-19 21:39:41 -0500 |
| commit | e4572e8c828cf5c82e072e01672283b5f27396a4 (patch) | |
| tree | e8cc8df65484f1f29d9e6bd0a27a362eadddd068 /setuptools | |
| parent | dcb24ad15465c266a3f258471766fbbe8fc8a42e (diff) | |
| parent | d1cef0c5aa0ca473d4b5fef6b11e5508a37663a2 (diff) | |
| download | python-setuptools-git-e4572e8c828cf5c82e072e01672283b5f27396a4.tar.gz | |
Merge branch 'master' into drop-py26
Diffstat (limited to 'setuptools')
24 files changed, 996 insertions, 436 deletions
diff --git a/setuptools/__init__.py b/setuptools/__init__.py index 04f76740..7da47fbe 100644 --- a/setuptools/__init__.py +++ b/setuptools/__init__.py @@ -109,7 +109,27 @@ class PEP420PackageFinder(PackageFinder): find_packages = PackageFinder.find -setup = distutils.core.setup + +def _install_setup_requires(attrs): + # Note: do not use `setuptools.Distribution` directly, as + # our PEP 517 backend patch `distutils.core.Distribution`. + dist = distutils.core.Distribution(dict( + (k, v) for k, v in attrs.items() + if k in ('dependency_links', 'setup_requires') + )) + # Honor setup.cfg's options. + dist.parse_config_files(ignore_option_errors=True) + if dist.setup_requires: + dist.fetch_build_eggs(dist.setup_requires) + + +def setup(**attrs): + # Make sure we have any requirements needed to interpret 'attrs'. + _install_setup_requires(attrs) + return distutils.core.setup(**attrs) + +setup.__doc__ = distutils.core.setup.__doc__ + _Command = monkey.get_unpatched(distutils.core.Command) diff --git a/setuptools/build_meta.py b/setuptools/build_meta.py new file mode 100644 index 00000000..609ea1e5 --- /dev/null +++ b/setuptools/build_meta.py @@ -0,0 +1,172 @@ +"""A PEP 517 interface to setuptools + +Previously, when a user or a command line tool (let's call it a "frontend") +needed to make a request of setuptools to take a certain action, for +example, generating a list of installation requirements, the frontend would +would call "setup.py egg_info" or "setup.py bdist_wheel" on the command line. + +PEP 517 defines a different method of interfacing with setuptools. Rather +than calling "setup.py" directly, the frontend should: + + 1. Set the current directory to the directory with a setup.py file + 2. Import this module into a safe python interpreter (one in which + setuptools can potentially set global variables or crash hard). + 3. Call one of the functions defined in PEP 517. + +What each function does is defined in PEP 517. However, here is a "casual" +definition of the functions (this definition should not be relied on for +bug reports or API stability): + + - `build_wheel`: build a wheel in the folder and return the basename + - `get_requires_for_build_wheel`: get the `setup_requires` to build + - `prepare_metadata_for_build_wheel`: get the `install_requires` + - `build_sdist`: build an sdist in the folder and return the basename + - `get_requires_for_build_sdist`: get the `setup_requires` to build + +Again, this is not a formal definition! Just a "taste" of the module. +""" + +import os +import sys +import tokenize +import shutil +import contextlib + +import setuptools +import distutils + + +class SetupRequirementsError(BaseException): + def __init__(self, specifiers): + self.specifiers = specifiers + + +class Distribution(setuptools.dist.Distribution): + def fetch_build_eggs(self, specifiers): + raise SetupRequirementsError(specifiers) + + @classmethod + @contextlib.contextmanager + def patch(cls): + """ + Replace + distutils.dist.Distribution with this class + for the duration of this context. + """ + orig = distutils.core.Distribution + distutils.core.Distribution = cls + try: + yield + finally: + distutils.core.Distribution = orig + + +def _run_setup(setup_script='setup.py'): + # Note that we can reuse our build directory between calls + # Correctness comes first, then optimization later + __file__ = setup_script + __name__ = '__main__' + f = getattr(tokenize, 'open', open)(__file__) + code = f.read().replace('\\r\\n', '\\n') + f.close() + exec(compile(code, __file__, 'exec'), locals()) + + +def _fix_config(config_settings): + config_settings = config_settings or {} + config_settings.setdefault('--global-option', []) + return config_settings + + +def _get_build_requires(config_settings): + config_settings = _fix_config(config_settings) + requirements = ['setuptools', 'wheel'] + + sys.argv = sys.argv[:1] + ['egg_info'] + \ + config_settings["--global-option"] + try: + with Distribution.patch(): + _run_setup() + except SetupRequirementsError as e: + requirements += e.specifiers + + return requirements + + +def _get_immediate_subdirectories(a_dir): + return [name for name in os.listdir(a_dir) + if os.path.isdir(os.path.join(a_dir, name))] + + +def get_requires_for_build_wheel(config_settings=None): + config_settings = _fix_config(config_settings) + return _get_build_requires(config_settings) + + +def get_requires_for_build_sdist(config_settings=None): + config_settings = _fix_config(config_settings) + return _get_build_requires(config_settings) + + +def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None): + sys.argv = sys.argv[:1] + ['dist_info', '--egg-base', metadata_directory] + _run_setup() + + dist_info_directory = metadata_directory + while True: + dist_infos = [f for f in os.listdir(dist_info_directory) + if f.endswith('.dist-info')] + + if len(dist_infos) == 0 and \ + len(_get_immediate_subdirectories(dist_info_directory)) == 1: + dist_info_directory = os.path.join( + dist_info_directory, os.listdir(dist_info_directory)[0]) + continue + + assert len(dist_infos) == 1 + break + + # PEP 517 requires that the .dist-info directory be placed in the + # metadata_directory. To comply, we MUST copy the directory to the root + if dist_info_directory != metadata_directory: + shutil.move( + os.path.join(dist_info_directory, dist_infos[0]), + metadata_directory) + shutil.rmtree(dist_info_directory, ignore_errors=True) + + return dist_infos[0] + + +def build_wheel(wheel_directory, config_settings=None, + metadata_directory=None): + config_settings = _fix_config(config_settings) + wheel_directory = os.path.abspath(wheel_directory) + sys.argv = sys.argv[:1] + ['bdist_wheel'] + \ + config_settings["--global-option"] + _run_setup() + if wheel_directory != 'dist': + shutil.rmtree(wheel_directory) + shutil.copytree('dist', wheel_directory) + + wheels = [f for f in os.listdir(wheel_directory) + if f.endswith('.whl')] + + assert len(wheels) == 1 + return wheels[0] + + +def build_sdist(sdist_directory, config_settings=None): + config_settings = _fix_config(config_settings) + sdist_directory = os.path.abspath(sdist_directory) + sys.argv = sys.argv[:1] + ['sdist'] + \ + config_settings["--global-option"] + _run_setup() + if sdist_directory != 'dist': + shutil.rmtree(sdist_directory) + shutil.copytree('dist', sdist_directory) + + sdists = [f for f in os.listdir(sdist_directory) + if f.endswith('.tar.gz')] + + assert len(sdists) == 1 + return sdists[0] diff --git a/setuptools/command/__init__.py b/setuptools/command/__init__.py index c96d33c2..fe619e2e 100644 --- a/setuptools/command/__init__.py +++ b/setuptools/command/__init__.py @@ -3,6 +3,7 @@ __all__ = [ 'easy_install', 'egg_info', 'install', 'install_lib', 'rotate', 'saveopts', 'sdist', 'setopt', 'test', 'install_egg_info', 'install_scripts', 'register', 'bdist_wininst', 'upload_docs', 'upload', 'build_clib', + 'dist_info', ] from distutils.command.bdist import bdist diff --git a/setuptools/command/bdist_egg.py b/setuptools/command/bdist_egg.py index 51755d52..5fdb62d9 100644 --- a/setuptools/command/bdist_egg.py +++ b/setuptools/command/bdist_egg.py @@ -8,6 +8,7 @@ from distutils import log from types import CodeType import sys import os +import re import textwrap import marshal @@ -240,11 +241,26 @@ class bdist_egg(Command): log.info("Removing .py files from temporary directory") for base, dirs, files in walk_egg(self.bdist_dir): for name in files: + path = os.path.join(base, name) + if name.endswith('.py'): - path = os.path.join(base, name) log.debug("Deleting %s", path) os.unlink(path) + if base.endswith('__pycache__'): + path_old = path + + pattern = r'(?P<name>.+)\.(?P<magic>[^.]+)\.pyc' + m = re.match(pattern, name) + path_new = os.path.join(base, os.pardir, m.group('name') + '.pyc') + log.info("Renaming file from [%s] to [%s]" % (path_old, path_new)) + try: + os.remove(path_new) + except OSError: + pass + os.rename(path_old, path_new) + + def zip_safe(self): safe = getattr(self.distribution, 'zip_safe', None) if safe is not None: diff --git a/setuptools/command/develop.py b/setuptools/command/develop.py index 85b23c60..959c932a 100755 --- a/setuptools/command/develop.py +++ b/setuptools/command/develop.py @@ -95,7 +95,9 @@ class develop(namespaces.DevelopInstaller, easy_install): 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)) + 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" diff --git a/setuptools/command/dist_info.py b/setuptools/command/dist_info.py new file mode 100644 index 00000000..c45258fa --- /dev/null +++ b/setuptools/command/dist_info.py @@ -0,0 +1,36 @@ +""" +Create a dist_info directory +As defined in the wheel specification +""" + +import os + +from distutils.core import Command +from distutils import log + + +class dist_info(Command): + + description = 'create a .dist-info directory' + + user_options = [ + ('egg-base=', 'e', "directory containing .egg-info directories" + " (default: top of the source tree)"), + ] + + def initialize_options(self): + self.egg_base = None + + def finalize_options(self): + pass + + def run(self): + egg_info = self.get_finalized_command('egg_info') + egg_info.egg_base = self.egg_base + egg_info.finalize_options() + egg_info.run() + dist_info_dir = egg_info.egg_info[:-len('.egg-info')] + '.dist-info' + log.info("creating '{}'".format(os.path.abspath(dist_info_dir))) + + bdist_wheel = self.get_finalized_command('bdist_wheel') + bdist_wheel.egg2dist(egg_info.egg_info, dist_info_dir) diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py index 8fba7b41..71991efa 100755 --- a/setuptools/command/easy_install.py +++ b/setuptools/command/easy_install.py @@ -1817,7 +1817,7 @@ def _update_zipimporter_cache(normalized_path, cache, updater=None): # get/del patterns instead. For more detailed information see the # following links: # https://github.com/pypa/setuptools/issues/202#issuecomment-202913420 - # https://bitbucket.org/pypy/pypy/src/dd07756a34a41f674c0cacfbc8ae1d4cc9ea2ae4/pypy/module/zipimport/interp_zipimport.py#cl-99 + # http://bit.ly/2h9itJX old_entry = cache[p] del cache[p] new_entry = updater and updater(p, old_entry) diff --git a/setuptools/command/test.py b/setuptools/command/test.py index f00d6794..51aee1f7 100644 --- a/setuptools/command/test.py +++ b/setuptools/command/test.py @@ -18,6 +18,11 @@ from setuptools import Command class ScanningLoader(TestLoader): + + def __init__(self): + TestLoader.__init__(self) + self._visited = set() + def loadTestsFromModule(self, module, pattern=None): """Return a suite of all tests cases contained in the given module @@ -25,6 +30,10 @@ class ScanningLoader(TestLoader): If the module has an ``additional_tests`` function, call it and add the return value to the tests. """ + if module in self._visited: + return None + self._visited.add(module) + tests = [] tests.append(TestLoader.loadTestsFromModule(self, module)) @@ -101,6 +110,8 @@ class test(Command): return list(self._test_args()) def _test_args(self): + if not self.test_suite and sys.version_info >= (2, 7): + yield 'discover' if self.verbose: yield '--verbose' if self.test_suite: diff --git a/setuptools/dist.py b/setuptools/dist.py index a2ca8795..aa304500 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -316,23 +316,19 @@ class Distribution(Distribution_parse_config_files, _Distribution): have_package_data = hasattr(self, "package_data") if not have_package_data: self.package_data = {} - _attrs_dict = attrs or {} - if 'features' in _attrs_dict or 'require_features' in _attrs_dict: + attrs = attrs or {} + if 'features' in attrs or 'require_features' in attrs: Feature.warn_deprecated() self.require_features = [] self.features = {} self.dist_files = [] - self.src_root = attrs and attrs.pop("src_root", None) + self.src_root = attrs.pop("src_root", None) self.patch_missing_pkg_info(attrs) - self.long_description_content_type = _attrs_dict.get( + self.long_description_content_type = attrs.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', []) - assert_string_list(self, 'dependency_links', self.dependency_links) - if attrs and 'setup_requires' in attrs: - self.fetch_build_eggs(attrs['setup_requires']) + self.dependency_links = attrs.pop('dependency_links', []) + self.setup_requires = attrs.pop('setup_requires', []) for ep in pkg_resources.iter_entry_points('distutils.setup_keywords'): vars(self).setdefault(ep.name, None) _Distribution.__init__(self, attrs) @@ -427,14 +423,15 @@ class Distribution(Distribution_parse_config_files, _Distribution): req.marker = None return req - def parse_config_files(self, filenames=None): + def parse_config_files(self, filenames=None, ignore_option_errors=False): """Parses configuration files from various levels and loads configuration. """ _Distribution.parse_config_files(self, filenames=filenames) - parse_configuration(self, self.command_options) + parse_configuration(self, self.command_options, + ignore_option_errors=ignore_option_errors) self._finalize_requires() def parse_command_line(self): @@ -497,19 +494,20 @@ class Distribution(Distribution_parse_config_files, _Distribution): """Fetch an egg needed for building""" 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 + opts.clear() + opts.update( + (k, v) + for k, v in self.get_option_dict('easy_install').items() + if k in ( + # don't use any other settings + 'find_links', 'site_dirs', 'index_url', + 'optimize', 'site_dirs', 'allow_hosts', + )) if self.dependency_links: links = self.dependency_links[:] if 'find_links' in opts: - links = opts['find_links'][1].split() + links + links = opts['find_links'][1] + links opts['find_links'] = ('setup', links) install_dir = self.get_egg_cache_dir() cmd = easy_install( diff --git a/setuptools/package_index.py b/setuptools/package_index.py index 4f610e0e..fe2ef50f 100755 --- a/setuptools/package_index.py +++ b/setuptools/package_index.py @@ -140,7 +140,7 @@ def distros_for_filename(filename, metadata=None): def interpret_distro_name( location, basename, metadata, py_version=None, precedence=SOURCE_DIST, platform=None - ): +): """Generate alternative interpretations of a source distro name Note: if `location` is a filesystem filename, you should call @@ -291,7 +291,7 @@ class PackageIndex(Environment): def __init__( self, index_url="https://pypi.python.org/simple", hosts=('*',), ca_bundle=None, verify_ssl=True, *args, **kw - ): + ): Environment.__init__(self, *args, **kw) self.index_url = index_url + "/" [:not index_url.endswith('/')] self.scanned_urls = {} @@ -345,7 +345,8 @@ class PackageIndex(Environment): base = f.url # handle redirects page = f.read() - if not isinstance(page, str): # We are in Python 3 and got bytes. We want str. + if not isinstance(page, str): + # In Python 3 and got bytes but want str. if isinstance(f, urllib.error.HTTPError): # Errors have no charset, assume latin1: charset = 'latin-1' @@ -380,8 +381,9 @@ class PackageIndex(Environment): is_file = s and s.group(1).lower() == 'file' if is_file or self.allows(urllib.parse.urlparse(url)[1]): return True - msg = ("\nNote: Bypassing %s (disallowed host; see " - "http://bit.ly/1dg9ijs for details).\n") + msg = ( + "\nNote: Bypassing %s (disallowed host; see " + "http://bit.ly/2hrImnY for details).\n") if fatal: raise DistutilsError(msg % url) else: @@ -499,15 +501,16 @@ class PackageIndex(Environment): """ checker is a ContentChecker """ - checker.report(self.debug, + checker.report( + self.debug, "Validating %%s checksum for %s" % filename) if not checker.is_valid(): tfp.close() os.unlink(filename) raise DistutilsError( "%s validation failed for %s; " - "possible download problem?" % ( - checker.hash.name, os.path.basename(filename)) + "possible download problem?" + % (checker.hash.name, os.path.basename(filename)) ) def add_find_links(self, urls): @@ -535,7 +538,8 @@ class PackageIndex(Environment): if self[requirement.key]: # we've seen at least one distro meth, msg = self.info, "Couldn't retrieve index page for %r" else: # no distros seen for this name, might be misspelled - meth, msg = (self.warn, + meth, msg = ( + self.warn, "Couldn't find index page for %r (maybe misspelled?)") meth(msg, requirement.unsafe_name) self.scan_all() @@ -576,8 +580,7 @@ class PackageIndex(Environment): def fetch_distribution( self, requirement, tmpdir, force_scan=False, source=False, - develop_ok=False, local_index=None - ): + develop_ok=False, local_index=None): """Obtain a distribution suitable for fulfilling `requirement` `requirement` must be a ``pkg_resources.Requirement`` instance. @@ -608,12 +611,19 @@ class PackageIndex(Environment): if dist.precedence == DEVELOP_DIST and not develop_ok: if dist not in skipped: - self.warn("Skipping development or system egg: %s", dist) + self.warn( + "Skipping development or system egg: %s", dist, + ) skipped[dist] = 1 continue - if dist in req and (dist.precedence <= SOURCE_DIST or not source): - dist.download_location = self.download(dist.location, tmpdir) + test = ( + dist in req + and (dist.precedence <= SOURCE_DIST or not source) + ) + if test: + loc = self.download(dist.location, tmpdir) + dist.download_location = loc if os.path.exists(dist.download_location): return dist @@ -703,7 +713,7 @@ class PackageIndex(Environment): def _download_to(self, url, filename): self.info("Downloading %s", url) # Download the file - fp, info = None, None + fp = None try: checker = HashChecker.from_url(url) fp = self.open_url(url) @@ -892,7 +902,7 @@ class PackageIndex(Environment): if rev is not None: self.info("Updating to %s", rev) - os.system("(cd %s && hg up -C -r %s >&-)" % ( + os.system("(cd %s && hg up -C -r %s -q)" % ( filename, rev, )) @@ -1102,7 +1112,8 @@ def local_open(url): f += '/' files.append('<a href="{name}">{name}</a>'.format(name=f)) else: - tmpl = ("<html><head><title>{url}</title>" + tmpl = ( + "<html><head><title>{url}</title>" "</head><body>{files}</body></html>") body = tmpl.format(url=url, files='\n'.join(files)) status, message = 200, "OK" diff --git a/setuptools/ssl_support.py b/setuptools/ssl_support.py index 72b18ef2..6362f1f4 100644 --- a/setuptools/ssl_support.py +++ b/setuptools/ssl_support.py @@ -186,9 +186,14 @@ class VerifyingHTTPSConn(HTTPSConnection): else: actual_host = self.host - self.sock = ssl.wrap_socket( - sock, cert_reqs=ssl.CERT_REQUIRED, ca_certs=self.ca_bundle - ) + if hasattr(ssl, 'create_default_context'): + ctx = ssl.create_default_context(cafile=self.ca_bundle) + self.sock = ctx.wrap_socket(sock, server_hostname=actual_host) + else: + # This is for python < 2.7.9 and < 3.4? + self.sock = ssl.wrap_socket( + sock, cert_reqs=ssl.CERT_REQUIRED, ca_certs=self.ca_bundle + ) try: match_hostname(self.sock.getpeercert(), actual_host) except CertificateError: diff --git a/setuptools/tests/__init__.py b/setuptools/tests/__init__.py index 8ae4402d..54dd7d2b 100644 --- a/setuptools/tests/__init__.py +++ b/setuptools/tests/__init__.py @@ -1,326 +1,6 @@ -"""Tests for the 'setuptools' package""" import locale -import sys -import os -import distutils.core -import distutils.cmd -from distutils.errors import DistutilsOptionError, DistutilsPlatformError -from distutils.errors import DistutilsSetupError -from distutils.core import Extension -from distutils.version import LooseVersion -from setuptools.extern import six import pytest -import setuptools.dist -import setuptools.depends as dep -from setuptools import Feature -from setuptools.depends import Require - is_ascii = locale.getpreferredencoding() == 'ANSI_X3.4-1968' fail_on_ascii = pytest.mark.xfail(is_ascii, reason="Test fails in this locale") - - -def makeSetup(**args): - """Return distribution from 'setup(**args)', without executing commands""" - - distutils.core._setup_stop_after = "commandline" - - # Don't let system command line leak into tests! - args.setdefault('script_args', ['install']) - - try: - return setuptools.setup(**args) - finally: - distutils.core._setup_stop_after = None - - -needs_bytecode = pytest.mark.skipif( - not hasattr(dep, 'get_module_constant'), - reason="bytecode support not available", -) - - -class TestDepends: - def testExtractConst(self): - if not hasattr(dep, 'extract_constant'): - # skip on non-bytecode platforms - return - - def f1(): - global x, y, z - x = "test" - y = z - - fc = six.get_function_code(f1) - - # unrecognized name - assert dep.extract_constant(fc, 'q', -1) is None - - # constant assigned - dep.extract_constant(fc, 'x', -1) == "test" - - # expression assigned - dep.extract_constant(fc, 'y', -1) == -1 - - # recognized name, not assigned - dep.extract_constant(fc, 'z', -1) is None - - def testFindModule(self): - with pytest.raises(ImportError): - dep.find_module('no-such.-thing') - with pytest.raises(ImportError): - dep.find_module('setuptools.non-existent') - f, p, i = dep.find_module('setuptools.tests') - f.close() - - @needs_bytecode - def testModuleExtract(self): - from email import __version__ - assert dep.get_module_constant('email', '__version__') == __version__ - assert dep.get_module_constant('sys', 'version') == sys.version - assert dep.get_module_constant('setuptools.tests', '__doc__') == __doc__ - - @needs_bytecode - def testRequire(self): - req = Require('Email', '1.0.3', 'email') - - assert req.name == 'Email' - assert req.module == 'email' - assert req.requested_version == '1.0.3' - assert req.attribute == '__version__' - assert req.full_name() == 'Email-1.0.3' - - from email import __version__ - assert req.get_version() == __version__ - assert req.version_ok('1.0.9') - assert not req.version_ok('0.9.1') - assert not req.version_ok('unknown') - - assert req.is_present() - assert req.is_current() - - req = Require('Email 3000', '03000', 'email', format=LooseVersion) - assert req.is_present() - assert not req.is_current() - assert not req.version_ok('unknown') - - req = Require('Do-what-I-mean', '1.0', 'd-w-i-m') - assert not req.is_present() - assert not req.is_current() - - req = Require('Tests', None, 'tests', homepage="http://example.com") - assert req.format is None - assert req.attribute is None - assert req.requested_version is None - assert req.full_name() == 'Tests' - assert req.homepage == 'http://example.com' - - paths = [os.path.dirname(p) for p in __path__] - assert req.is_present(paths) - assert req.is_current(paths) - - -class TestDistro: - def setup_method(self, method): - self.e1 = Extension('bar.ext', ['bar.c']) - self.e2 = Extension('c.y', ['y.c']) - - self.dist = makeSetup( - packages=['a', 'a.b', 'a.b.c', 'b', 'c'], - py_modules=['b.d', 'x'], - ext_modules=(self.e1, self.e2), - package_dir={}, - ) - - def testDistroType(self): - assert isinstance(self.dist, setuptools.dist.Distribution) - - def testExcludePackage(self): - self.dist.exclude_package('a') - assert self.dist.packages == ['b', 'c'] - - self.dist.exclude_package('b') - assert self.dist.packages == ['c'] - assert self.dist.py_modules == ['x'] - assert self.dist.ext_modules == [self.e1, self.e2] - - self.dist.exclude_package('c') - assert self.dist.packages == [] - assert self.dist.py_modules == ['x'] - assert self.dist.ext_modules == [self.e1] - - # test removals from unspecified options - makeSetup().exclude_package('x') - - def testIncludeExclude(self): - # remove an extension - self.dist.exclude(ext_modules=[self.e1]) - assert self.dist.ext_modules == [self.e2] - - # add it back in - self.dist.include(ext_modules=[self.e1]) - assert self.dist.ext_modules == [self.e2, self.e1] - - # should not add duplicate - self.dist.include(ext_modules=[self.e1]) - assert self.dist.ext_modules == [self.e2, self.e1] - - def testExcludePackages(self): - self.dist.exclude(packages=['c', 'b', 'a']) - assert self.dist.packages == [] - assert self.dist.py_modules == ['x'] - assert self.dist.ext_modules == [self.e1] - - def testEmpty(self): - dist = makeSetup() - dist.include(packages=['a'], py_modules=['b'], ext_modules=[self.e2]) - dist = makeSetup() - dist.exclude(packages=['a'], py_modules=['b'], ext_modules=[self.e2]) - - def testContents(self): - assert self.dist.has_contents_for('a') - self.dist.exclude_package('a') - assert not self.dist.has_contents_for('a') - - assert self.dist.has_contents_for('b') - self.dist.exclude_package('b') - assert not self.dist.has_contents_for('b') - - assert self.dist.has_contents_for('c') - self.dist.exclude_package('c') - assert not self.dist.has_contents_for('c') - - def testInvalidIncludeExclude(self): - with pytest.raises(DistutilsSetupError): - self.dist.include(nonexistent_option='x') - with pytest.raises(DistutilsSetupError): - self.dist.exclude(nonexistent_option='x') - with pytest.raises(DistutilsSetupError): - self.dist.include(packages={'x': 'y'}) - with pytest.raises(DistutilsSetupError): - self.dist.exclude(packages={'x': 'y'}) - with pytest.raises(DistutilsSetupError): - self.dist.include(ext_modules={'x': 'y'}) - with pytest.raises(DistutilsSetupError): - self.dist.exclude(ext_modules={'x': 'y'}) - - with pytest.raises(DistutilsSetupError): - self.dist.include(package_dir=['q']) - with pytest.raises(DistutilsSetupError): - self.dist.exclude(package_dir=['q']) - - -class TestFeatures: - def setup_method(self, method): - self.req = Require('Distutils', '1.0.3', 'distutils') - self.dist = makeSetup( - features={ - 'foo': Feature("foo", standard=True, require_features=['baz', self.req]), - 'bar': Feature("bar", standard=True, packages=['pkg.bar'], - py_modules=['bar_et'], remove=['bar.ext'], - ), - 'baz': Feature( - "baz", optional=False, packages=['pkg.baz'], - scripts=['scripts/baz_it'], - libraries=[('libfoo', 'foo/foofoo.c')] - ), - 'dwim': Feature("DWIM", available=False, remove='bazish'), - }, - script_args=['--without-bar', 'install'], - packages=['pkg.bar', 'pkg.foo'], - py_modules=['bar_et', 'bazish'], - ext_modules=[Extension('bar.ext', ['bar.c'])] - ) - - def testDefaults(self): - assert not Feature( - "test", standard=True, remove='x', available=False - ).include_by_default() - assert Feature("test", standard=True, remove='x').include_by_default() - # Feature must have either kwargs, removes, or require_features - with pytest.raises(DistutilsSetupError): - Feature("test") - - def testAvailability(self): - with pytest.raises(DistutilsPlatformError): - self.dist.features['dwim'].include_in(self.dist) - - def testFeatureOptions(self): - dist = self.dist - assert ( - ('with-dwim', None, 'include DWIM') in dist.feature_options - ) - assert ( - ('without-dwim', None, 'exclude DWIM (default)') in dist.feature_options - ) - assert ( - ('with-bar', None, 'include bar (default)') in dist.feature_options - ) - assert ( - ('without-bar', None, 'exclude bar') in dist.feature_options - ) - assert dist.feature_negopt['without-foo'] == 'with-foo' - assert dist.feature_negopt['without-bar'] == 'with-bar' - assert dist.feature_negopt['without-dwim'] == 'with-dwim' - assert ('without-baz' not in dist.feature_negopt) - - def testUseFeatures(self): - dist = self.dist - assert dist.with_foo == 1 - assert dist.with_bar == 0 - assert dist.with_baz == 1 - assert ('bar_et' not in dist.py_modules) - assert ('pkg.bar' not in dist.packages) - assert ('pkg.baz' in dist.packages) - assert ('scripts/baz_it' in dist.scripts) - assert (('libfoo', 'foo/foofoo.c') in dist.libraries) - assert dist.ext_modules == [] - assert dist.require_features == [self.req] - - # If we ask for bar, it should fail because we explicitly disabled - # it on the command line - with pytest.raises(DistutilsOptionError): - dist.include_feature('bar') - - def testFeatureWithInvalidRemove(self): - with pytest.raises(SystemExit): - makeSetup(features={'x': Feature('x', remove='y')}) - - -class TestCommandTests: - def testTestIsCommand(self): - test_cmd = makeSetup().get_command_obj('test') - assert (isinstance(test_cmd, distutils.cmd.Command)) - - def testLongOptSuiteWNoDefault(self): - ts1 = makeSetup(script_args=['test', '--test-suite=foo.tests.suite']) - ts1 = ts1.get_command_obj('test') - ts1.ensure_finalized() - assert ts1.test_suite == 'foo.tests.suite' - - def testDefaultSuite(self): - ts2 = makeSetup(test_suite='bar.tests.suite').get_command_obj('test') - ts2.ensure_finalized() - assert ts2.test_suite == 'bar.tests.suite' - - def testDefaultWModuleOnCmdLine(self): - ts3 = makeSetup( - test_suite='bar.tests', - script_args=['test', '-m', 'foo.tests'] - ).get_command_obj('test') - ts3.ensure_finalized() - assert ts3.test_module == 'foo.tests' - assert ts3.test_suite == 'foo.tests.test_suite' - - def testConflictingOptions(self): - ts4 = makeSetup( - script_args=['test', '-m', 'bar.tests', '-s', 'foo.tests.suite'] - ).get_command_obj('test') - with pytest.raises(DistutilsOptionError): - ts4.ensure_finalized() - - def testNoSuite(self): - ts5 = makeSetup().get_command_obj('test') - ts5.ensure_finalized() - assert ts5.test_suite is None diff --git a/setuptools/tests/test_bdist_egg.py b/setuptools/tests/test_bdist_egg.py index d24aa366..54742aa6 100644 --- a/setuptools/tests/test_bdist_egg.py +++ b/setuptools/tests/test_bdist_egg.py @@ -2,6 +2,7 @@ """ import os import re +import zipfile import pytest @@ -16,7 +17,7 @@ setup(name='foo', py_modules=['hi']) """ -@pytest.yield_fixture +@pytest.fixture(scope='function') def setup_context(tmpdir): with (tmpdir / 'setup.py').open('w') as f: f.write(SETUP_PY) @@ -32,7 +33,7 @@ class Test: script_name='setup.py', script_args=['bdist_egg'], name='foo', - py_modules=['hi'] + py_modules=['hi'], )) os.makedirs(os.path.join('build', 'src')) with contexts.quiet(): @@ -42,3 +43,24 @@ class Test: # let's see if we got our egg link at the right place [content] = os.listdir('dist') assert re.match(r'foo-0.0.0-py[23].\d.egg$', content) + + @pytest.mark.xfail( + os.environ.get('PYTHONDONTWRITEBYTECODE'), + reason="Byte code disabled", + ) + def test_exclude_source_files(self, setup_context, user_override): + dist = Distribution(dict( + script_name='setup.py', + script_args=['bdist_egg', '--exclude-source-files'], + name='foo', + py_modules=['hi'], + )) + with contexts.quiet(): + dist.parse_command_line() + dist.run_commands() + [dist_name] = os.listdir('dist') + dist_filename = os.path.join('dist', dist_name) + zip = zipfile.ZipFile(dist_filename) + names = list(zi.filename for zi in zip.filelist) + assert 'hi.pyc' in names + assert 'hi.py' not in names diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py new file mode 100644 index 00000000..659c1a65 --- /dev/null +++ b/setuptools/tests/test_build_meta.py @@ -0,0 +1,126 @@ +import os + +import pytest + +from .files import build_files +from .textwrap import DALS + + +futures = pytest.importorskip('concurrent.futures') +importlib = pytest.importorskip('importlib') + + +class BuildBackendBase(object): + def __init__(self, cwd=None, env={}, backend_name='setuptools.build_meta'): + self.cwd = cwd + self.env = env + self.backend_name = backend_name + + +class BuildBackend(BuildBackendBase): + """PEP 517 Build Backend""" + def __init__(self, *args, **kwargs): + super(BuildBackend, self).__init__(*args, **kwargs) + self.pool = futures.ProcessPoolExecutor() + + def __getattr__(self, name): + """Handles aribrary function invocations on the build backend.""" + def method(*args, **kw): + root = os.path.abspath(self.cwd) + caller = BuildBackendCaller(root, self.env, self.backend_name) + return self.pool.submit(caller, name, *args, **kw).result() + + return method + + +class BuildBackendCaller(BuildBackendBase): + def __call__(self, name, *args, **kw): + """Handles aribrary function invocations on the build backend.""" + os.chdir(self.cwd) + os.environ.update(self.env) + mod = importlib.import_module(self.backend_name) + return getattr(mod, name)(*args, **kw) + + +defns = [{ + 'setup.py': DALS(""" + __import__('setuptools').setup( + name='foo', + py_modules=['hello'], + setup_requires=['six'], + ) + """), + 'hello.py': DALS(""" + def run(): + print('hello') + """), + }, + { + 'setup.py': DALS(""" + assert __name__ == '__main__' + __import__('setuptools').setup( + name='foo', + py_modules=['hello'], + setup_requires=['six'], + ) + """), + 'hello.py': DALS(""" + def run(): + print('hello') + """), + }, + { + 'setup.py': DALS(""" + variable = True + def function(): + return variable + assert variable + __import__('setuptools').setup( + name='foo', + py_modules=['hello'], + setup_requires=['six'], + ) + """), + 'hello.py': DALS(""" + def run(): + print('hello') + """), + }] + + +@pytest.fixture(params=defns) +def build_backend(tmpdir, request): + build_files(request.param, prefix=str(tmpdir)) + with tmpdir.as_cwd(): + yield BuildBackend(cwd='.') + + +def test_get_requires_for_build_wheel(build_backend): + actual = build_backend.get_requires_for_build_wheel() + expected = ['six', 'setuptools', 'wheel'] + assert sorted(actual) == sorted(expected) + + +def test_build_wheel(build_backend): + dist_dir = os.path.abspath('pip-wheel') + os.makedirs(dist_dir) + wheel_name = build_backend.build_wheel(dist_dir) + + assert os.path.isfile(os.path.join(dist_dir, wheel_name)) + + +def test_build_sdist(build_backend): + dist_dir = os.path.abspath('pip-sdist') + os.makedirs(dist_dir) + sdist_name = build_backend.build_sdist(dist_dir) + + assert os.path.isfile(os.path.join(dist_dir, sdist_name)) + + +def test_prepare_metadata_for_build_wheel(build_backend): + dist_dir = os.path.abspath('pip-dist-info') + os.makedirs(dist_dir) + + dist_info = build_backend.prepare_metadata_for_build_wheel(dist_dir) + + assert os.path.isfile(os.path.join(dist_dir, dist_info, 'METADATA')) diff --git a/setuptools/tests/test_develop.py b/setuptools/tests/test_develop.py index ad7cfa05..cb4ff4b4 100644 --- a/setuptools/tests/test_develop.py +++ b/setuptools/tests/test_develop.py @@ -167,7 +167,9 @@ class TestNamespaces: target = tmpdir / 'packages' # use pip to install to the target directory install_cmd = [ - 'pip', + sys.executable, + '-m', + 'pip.__main__', 'install', str(pkg_A), '-t', str(target), diff --git a/setuptools/tests/test_dist.py b/setuptools/tests/test_dist.py index 435ffec0..c4c9bd03 100644 --- a/setuptools/tests/test_dist.py +++ b/setuptools/tests/test_dist.py @@ -39,6 +39,7 @@ def test_dist_fetch_build_egg(tmpdir): '''.split() with tmpdir.as_cwd(): dist = Distribution() + dist.parse_config_files() resolved_dists = [ dist.fetch_build_egg(r) for r in reqs diff --git a/setuptools/tests/test_easy_install.py b/setuptools/tests/test_easy_install.py index e4ed556f..834710ef 100644 --- a/setuptools/tests/test_easy_install.py +++ b/setuptools/tests/test_easy_install.py @@ -380,7 +380,15 @@ class TestSetupRequires: """))]) yield dist_path - def test_setup_requires_overrides_version_conflict(self): + use_setup_cfg = ( + (), + ('dependency_links',), + ('setup_requires',), + ('dependency_links', 'setup_requires'), + ) + + @pytest.mark.parametrize('use_setup_cfg', use_setup_cfg) + def test_setup_requires_overrides_version_conflict(self, use_setup_cfg): """ Regression test for distribution issue 323: https://bitbucket.org/tarek/distribute/issues/323 @@ -396,7 +404,7 @@ class TestSetupRequires: with contexts.save_pkg_resources_state(): with contexts.tempdir() as temp_dir: - test_pkg = create_setup_requires_package(temp_dir) + test_pkg = create_setup_requires_package(temp_dir, use_setup_cfg=use_setup_cfg) test_setup_py = os.path.join(test_pkg, 'setup.py') with contexts.quiet() as (stdout, stderr): # Don't even need to install the package, just @@ -405,9 +413,10 @@ class TestSetupRequires: lines = stdout.readlines() assert len(lines) > 0 - assert lines[-1].strip(), 'test_pkg' + assert lines[-1].strip() == 'test_pkg' - def test_setup_requires_override_nspkg(self): + @pytest.mark.parametrize('use_setup_cfg', use_setup_cfg) + def test_setup_requires_override_nspkg(self, use_setup_cfg): """ Like ``test_setup_requires_overrides_version_conflict`` but where the ``setup_requires`` package is part of a namespace package that has @@ -445,7 +454,8 @@ class TestSetupRequires: """) test_pkg = create_setup_requires_package( - temp_dir, 'foo.bar', '0.2', make_nspkg_sdist, template) + temp_dir, 'foo.bar', '0.2', make_nspkg_sdist, template, + use_setup_cfg=use_setup_cfg) test_setup_py = os.path.join(test_pkg, 'setup.py') @@ -463,6 +473,38 @@ class TestSetupRequires: assert len(lines) > 0 assert lines[-1].strip() == 'test_pkg' + @pytest.mark.parametrize('use_setup_cfg', use_setup_cfg) + def test_setup_requires_with_attr_version(self, use_setup_cfg): + def make_dependency_sdist(dist_path, distname, version): + make_sdist(dist_path, [ + ('setup.py', + DALS(""" + import setuptools + setuptools.setup( + name={name!r}, + version={version!r}, + py_modules=[{name!r}], + ) + """.format(name=distname, version=version))), + (distname + '.py', + DALS(""" + version = 42 + """ + ))]) + with contexts.save_pkg_resources_state(): + with contexts.tempdir() as temp_dir: + test_pkg = create_setup_requires_package( + temp_dir, setup_attrs=dict(version='attr: foobar.version'), + make_package=make_dependency_sdist, + use_setup_cfg=use_setup_cfg+('version',), + ) + test_setup_py = os.path.join(test_pkg, 'setup.py') + with contexts.quiet() as (stdout, stderr): + run_setup(test_setup_py, ['--version']) + lines = stdout.readlines() + assert len(lines) > 0 + assert lines[-1].strip() == '42' + def make_trivial_sdist(dist_path, distname, version): """ @@ -531,7 +573,8 @@ def make_sdist(dist_path, files): def create_setup_requires_package(path, distname='foobar', version='0.1', make_package=make_trivial_sdist, - setup_py_template=None): + setup_py_template=None, setup_attrs={}, + use_setup_cfg=()): """Creates a source tree under path for a trivial test package that has a single requirement in setup_requires--a tarball for that requirement is also created and added to the dependency_links argument. @@ -546,11 +589,39 @@ def create_setup_requires_package(path, distname='foobar', version='0.1', 'setup_requires': ['%s==%s' % (distname, version)], 'dependency_links': [os.path.abspath(path)] } + test_setup_attrs.update(setup_attrs) test_pkg = os.path.join(path, 'test_pkg') - test_setup_py = os.path.join(test_pkg, 'setup.py') os.mkdir(test_pkg) + if use_setup_cfg: + test_setup_cfg = os.path.join(test_pkg, 'setup.cfg') + options = [] + metadata = [] + for name in use_setup_cfg: + value = test_setup_attrs.pop(name) + if name in 'name version'.split(): + section = metadata + else: + section = options + if isinstance(value, (tuple, list)): + value = ';'.join(value) + section.append('%s: %s' % (name, value)) + with open(test_setup_cfg, 'w') as f: + f.write(DALS( + """ + [metadata] + {metadata} + [options] + {options} + """ + ).format( + options='\n'.join(options), + metadata='\n'.join(metadata), + )) + + test_setup_py = os.path.join(test_pkg, 'setup.py') + if setup_py_template is None: setup_py_template = DALS("""\ import setuptools diff --git a/setuptools/tests/test_egg_info.py b/setuptools/tests/test_egg_info.py index 4c04d298..a97d0c84 100644 --- a/setuptools/tests/test_egg_info.py +++ b/setuptools/tests/test_egg_info.py @@ -157,7 +157,8 @@ class TestEggInfo(object): self._run_install_command(tmpdir_cwd, env) egg_info_dir = self._find_egg_info_files(env.paths['lib']).base sources_txt = os.path.join(egg_info_dir, 'SOURCES.txt') - assert 'docs/usage.rst' in open(sources_txt).read().split('\n') + with open(sources_txt) as f: + assert 'docs/usage.rst' in f.read().split('\n') def _setup_script_with_requires(self, requires, use_setup_cfg=False): setup_script = DALS( @@ -440,7 +441,8 @@ class TestEggInfo(object): self._run_install_command(tmpdir_cwd, env) egg_info_dir = self._find_egg_info_files(env.paths['lib']).base pkginfo = os.path.join(egg_info_dir, 'PKG-INFO') - assert 'Requires-Python: >=1.2.3' in open(pkginfo).read().split('\n') + with open(pkginfo) as f: + assert 'Requires-Python: >=1.2.3' in f.read().split('\n') def test_manifest_maker_warning_suppression(self): fixtures = [ diff --git a/setuptools/tests/test_namespaces.py b/setuptools/tests/test_namespaces.py index 721cad1e..1ac1b35e 100644 --- a/setuptools/tests/test_namespaces.py +++ b/setuptools/tests/test_namespaces.py @@ -30,7 +30,9 @@ class TestNamespaces: targets = site_packages, path_packages # use pip to install to the target directory install_cmd = [ - 'pip', + sys.executable, + '-m', + 'pip.__main__', 'install', str(pkg_A), '-t', str(site_packages), @@ -38,7 +40,9 @@ class TestNamespaces: subprocess.check_call(install_cmd) namespaces.make_site_dir(site_packages) install_cmd = [ - 'pip', + sys.executable, + '-m', + 'pip.__main__', 'install', str(pkg_B), '-t', str(path_packages), @@ -88,7 +92,9 @@ class TestNamespaces: target = tmpdir / 'packages' # use pip to install to the target directory install_cmd = [ - 'pip', + sys.executable, + '-m', + 'pip.__main__', 'install', str(pkg_A), '-t', str(target), diff --git a/setuptools/tests/test_sdist.py b/setuptools/tests/test_sdist.py index f34068dc..02222da5 100644 --- a/setuptools/tests/test_sdist.py +++ b/setuptools/tests/test_sdist.py @@ -19,6 +19,7 @@ from setuptools.command.sdist import sdist from setuptools.command.egg_info import manifest_maker from setuptools.dist import Distribution from setuptools.tests import fail_on_ascii +from .text import Filenames py3_only = pytest.mark.xfail(six.PY2, reason="Test runs on Python 3 only") @@ -36,13 +37,7 @@ from setuptools import setup setup(**%r) """ % SETUP_ATTRS -if six.PY3: - LATIN1_FILENAME = 'smörbröd.py'.encode('latin-1') -else: - LATIN1_FILENAME = 'sm\xf6rbr\xf6d.py' - -# Cannot use context manager because of Python 2.4 @contextlib.contextmanager def quiet(): old_stdout, old_stderr = sys.stdout, sys.stderr @@ -53,17 +48,10 @@ def quiet(): sys.stdout, sys.stderr = old_stdout, old_stderr -# Fake byte literals for Python <= 2.5 -def b(s, encoding='utf-8'): - if six.PY3: - return s.encode(encoding) - return s - - # Convert to POSIX path def posix(path): if six.PY3 and not isinstance(path, str): - return path.replace(os.sep.encode('ascii'), b('/')) + return path.replace(os.sep.encode('ascii'), b'/') else: return path.replace(os.sep, '/') @@ -86,6 +74,21 @@ def read_all_bytes(filename): return fp.read() +def latin1_fail(): + try: + desc, filename = tempfile.mkstemp(suffix=Filenames.latin_1) + os.close(desc) + os.remove(filename) + except Exception: + return True + + +fail_on_latin1_encoded_filenames = pytest.mark.xfail( + latin1_fail(), + reason="System does not support latin-1 filenames", +) + + class TestSdistTest: def setup_method(self, method): self.temp_dir = tempfile.mkdtemp() @@ -134,8 +137,8 @@ class TestSdistTest: def test_defaults_case_sensitivity(self): """ - Make sure default files (README.*, etc.) are added in a case-sensitive - way to avoid problems with packages built on Windows. + Make sure default files (README.*, etc.) are added in a case-sensitive + way to avoid problems with packages built on Windows. """ open(os.path.join(self.temp_dir, 'readme.rst'), 'w').close() @@ -152,7 +155,9 @@ class TestSdistTest: with quiet(): cmd.run() - # lowercase all names so we can test in a case-insensitive way to make sure the files are not included + # lowercase all names so we can test in a + # case-insensitive way to make sure the files + # are not included. manifest = map(lambda x: x.lower(), cmd.filelist.files) assert 'readme.rst' not in manifest, manifest assert 'setup.py' not in manifest, manifest @@ -201,8 +206,7 @@ class TestSdistTest: mm.manifest = os.path.join('sdist_test.egg-info', 'SOURCES.txt') os.mkdir('sdist_test.egg-info') - # UTF-8 filename - filename = os.path.join(b('sdist_test'), b('smörbröd.py')) + filename = os.path.join(b'sdist_test', Filenames.utf_8) # Must touch the file or risk removal open(filename, "w").close() @@ -241,7 +245,7 @@ class TestSdistTest: os.mkdir('sdist_test.egg-info') # Latin-1 filename - filename = os.path.join(b('sdist_test'), LATIN1_FILENAME) + filename = os.path.join(b'sdist_test', Filenames.latin_1) # Add filename with surrogates and write manifest with quiet(): @@ -275,10 +279,10 @@ class TestSdistTest: cmd.run() # Add UTF-8 filename to manifest - filename = os.path.join(b('sdist_test'), b('smörbröd.py')) + filename = os.path.join(b'sdist_test', Filenames.utf_8) cmd.manifest = os.path.join('sdist_test.egg-info', 'SOURCES.txt') manifest = open(cmd.manifest, 'ab') - manifest.write(b('\n') + filename) + manifest.write(b'\n' + filename) manifest.close() # The file must exist to be included in the filelist @@ -295,6 +299,7 @@ class TestSdistTest: assert filename in cmd.filelist.files @py3_only + @fail_on_latin1_encoded_filenames def test_read_manifest_skips_non_utf8_filenames(self): # Test for #303. dist = Distribution(SETUP_ATTRS) @@ -307,10 +312,10 @@ class TestSdistTest: cmd.run() # Add Latin-1 filename to manifest - filename = os.path.join(b('sdist_test'), LATIN1_FILENAME) + filename = os.path.join(b'sdist_test', Filenames.latin_1) cmd.manifest = os.path.join('sdist_test.egg-info', 'SOURCES.txt') manifest = open(cmd.manifest, 'ab') - manifest.write(b('\n') + filename) + manifest.write(b'\n' + filename) manifest.close() # The file must exist to be included in the filelist @@ -326,6 +331,7 @@ class TestSdistTest: assert filename not in cmd.filelist.files @fail_on_ascii + @fail_on_latin1_encoded_filenames def test_sdist_with_utf8_encoded_filename(self): # Test for #303. dist = Distribution(SETUP_ATTRS) @@ -333,8 +339,7 @@ class TestSdistTest: cmd = sdist(dist) cmd.ensure_finalized() - # UTF-8 filename - filename = os.path.join(b('sdist_test'), b('smörbröd.py')) + filename = os.path.join(b'sdist_test', Filenames.utf_8) open(filename, 'w').close() with quiet(): @@ -360,6 +365,7 @@ class TestSdistTest: else: assert filename in cmd.filelist.files + @fail_on_latin1_encoded_filenames def test_sdist_with_latin1_encoded_filename(self): # Test for #303. dist = Distribution(SETUP_ATTRS) @@ -368,7 +374,7 @@ class TestSdistTest: cmd.ensure_finalized() # Latin-1 filename - filename = os.path.join(b('sdist_test'), LATIN1_FILENAME) + filename = os.path.join(b'sdist_test', Filenames.latin_1) open(filename, 'w').close() assert os.path.isfile(filename) @@ -381,10 +387,9 @@ class TestSdistTest: # Latin-1 is similar to Windows-1252 however # on mbcs filesys it is not in latin-1 encoding fs_enc = sys.getfilesystemencoding() - if fs_enc == 'mbcs': - filename = filename.decode('mbcs') - else: - filename = filename.decode('latin-1') + if fs_enc != 'mbcs': + fs_enc = 'latin-1' + filename = filename.decode(fs_enc) assert filename in cmd.filelist.files else: diff --git a/setuptools/tests/test_setuptools.py b/setuptools/tests/test_setuptools.py index e59800d2..26e37a6c 100644 --- a/setuptools/tests/test_setuptools.py +++ b/setuptools/tests/test_setuptools.py @@ -1,8 +1,328 @@ +"""Tests for the 'setuptools' package""" + +import sys import os +import distutils.core +import distutils.cmd +from distutils.errors import DistutilsOptionError, DistutilsPlatformError +from distutils.errors import DistutilsSetupError +from distutils.core import Extension +from distutils.version import LooseVersion import pytest import setuptools +import setuptools.dist +import setuptools.depends as dep +from setuptools import Feature +from setuptools.depends import Require +from setuptools.extern import six + + +def makeSetup(**args): + """Return distribution from 'setup(**args)', without executing commands""" + + distutils.core._setup_stop_after = "commandline" + + # Don't let system command line leak into tests! + args.setdefault('script_args', ['install']) + + try: + return setuptools.setup(**args) + finally: + distutils.core._setup_stop_after = None + + +needs_bytecode = pytest.mark.skipif( + not hasattr(dep, 'get_module_constant'), + reason="bytecode support not available", +) + + +class TestDepends: + def testExtractConst(self): + if not hasattr(dep, 'extract_constant'): + # skip on non-bytecode platforms + return + + def f1(): + global x, y, z + x = "test" + y = z + + fc = six.get_function_code(f1) + + # unrecognized name + assert dep.extract_constant(fc, 'q', -1) is None + + # constant assigned + dep.extract_constant(fc, 'x', -1) == "test" + + # expression assigned + dep.extract_constant(fc, 'y', -1) == -1 + + # recognized name, not assigned + dep.extract_constant(fc, 'z', -1) is None + + def testFindModule(self): + with pytest.raises(ImportError): + dep.find_module('no-such.-thing') + with pytest.raises(ImportError): + dep.find_module('setuptools.non-existent') + f, p, i = dep.find_module('setuptools.tests') + f.close() + + @needs_bytecode + def testModuleExtract(self): + from json import __version__ + assert dep.get_module_constant('json', '__version__') == __version__ + assert dep.get_module_constant('sys', 'version') == sys.version + assert dep.get_module_constant('setuptools.tests.test_setuptools', '__doc__') == __doc__ + + @needs_bytecode + def testRequire(self): + req = Require('Json', '1.0.3', 'json') + + assert req.name == 'Json' + assert req.module == 'json' + assert req.requested_version == '1.0.3' + assert req.attribute == '__version__' + assert req.full_name() == 'Json-1.0.3' + + from json import __version__ + assert req.get_version() == __version__ + assert req.version_ok('1.0.9') + assert not req.version_ok('0.9.1') + assert not req.version_ok('unknown') + + assert req.is_present() + assert req.is_current() + + req = Require('Json 3000', '03000', 'json', format=LooseVersion) + assert req.is_present() + assert not req.is_current() + assert not req.version_ok('unknown') + + req = Require('Do-what-I-mean', '1.0', 'd-w-i-m') + assert not req.is_present() + assert not req.is_current() + + req = Require('Tests', None, 'tests', homepage="http://example.com") + assert req.format is None + assert req.attribute is None + assert req.requested_version is None + assert req.full_name() == 'Tests' + assert req.homepage == 'http://example.com' + + from setuptools.tests import __path__ + paths = [os.path.dirname(p) for p in __path__] + assert req.is_present(paths) + assert req.is_current(paths) + + +class TestDistro: + def setup_method(self, method): + self.e1 = Extension('bar.ext', ['bar.c']) + self.e2 = Extension('c.y', ['y.c']) + + self.dist = makeSetup( + packages=['a', 'a.b', 'a.b.c', 'b', 'c'], + py_modules=['b.d', 'x'], + ext_modules=(self.e1, self.e2), + package_dir={}, + ) + + def testDistroType(self): + assert isinstance(self.dist, setuptools.dist.Distribution) + + def testExcludePackage(self): + self.dist.exclude_package('a') + assert self.dist.packages == ['b', 'c'] + + self.dist.exclude_package('b') + assert self.dist.packages == ['c'] + assert self.dist.py_modules == ['x'] + assert self.dist.ext_modules == [self.e1, self.e2] + + self.dist.exclude_package('c') + assert self.dist.packages == [] + assert self.dist.py_modules == ['x'] + assert self.dist.ext_modules == [self.e1] + + # test removals from unspecified options + makeSetup().exclude_package('x') + + def testIncludeExclude(self): + # remove an extension + self.dist.exclude(ext_modules=[self.e1]) + assert self.dist.ext_modules == [self.e2] + + # add it back in + self.dist.include(ext_modules=[self.e1]) + assert self.dist.ext_modules == [self.e2, self.e1] + + # should not add duplicate + self.dist.include(ext_modules=[self.e1]) + assert self.dist.ext_modules == [self.e2, self.e1] + + def testExcludePackages(self): + self.dist.exclude(packages=['c', 'b', 'a']) + assert self.dist.packages == [] + assert self.dist.py_modules == ['x'] + assert self.dist.ext_modules == [self.e1] + + def testEmpty(self): + dist = makeSetup() + dist.include(packages=['a'], py_modules=['b'], ext_modules=[self.e2]) + dist = makeSetup() + dist.exclude(packages=['a'], py_modules=['b'], ext_modules=[self.e2]) + + def testContents(self): + assert self.dist.has_contents_for('a') + self.dist.exclude_package('a') + assert not self.dist.has_contents_for('a') + + assert self.dist.has_contents_for('b') + self.dist.exclude_package('b') + assert not self.dist.has_contents_for('b') + + assert self.dist.has_contents_for('c') + self.dist.exclude_package('c') + assert not self.dist.has_contents_for('c') + + def testInvalidIncludeExclude(self): + with pytest.raises(DistutilsSetupError): + self.dist.include(nonexistent_option='x') + with pytest.raises(DistutilsSetupError): + self.dist.exclude(nonexistent_option='x') + with pytest.raises(DistutilsSetupError): + self.dist.include(packages={'x': 'y'}) + with pytest.raises(DistutilsSetupError): + self.dist.exclude(packages={'x': 'y'}) + with pytest.raises(DistutilsSetupError): + self.dist.include(ext_modules={'x': 'y'}) + with pytest.raises(DistutilsSetupError): + self.dist.exclude(ext_modules={'x': 'y'}) + + with pytest.raises(DistutilsSetupError): + self.dist.include(package_dir=['q']) + with pytest.raises(DistutilsSetupError): + self.dist.exclude(package_dir=['q']) + + +class TestFeatures: + def setup_method(self, method): + self.req = Require('Distutils', '1.0.3', 'distutils') + self.dist = makeSetup( + features={ + 'foo': Feature("foo", standard=True, require_features=['baz', self.req]), + 'bar': Feature("bar", standard=True, packages=['pkg.bar'], + py_modules=['bar_et'], remove=['bar.ext'], + ), + 'baz': Feature( + "baz", optional=False, packages=['pkg.baz'], + scripts=['scripts/baz_it'], + libraries=[('libfoo', 'foo/foofoo.c')] + ), + 'dwim': Feature("DWIM", available=False, remove='bazish'), + }, + script_args=['--without-bar', 'install'], + packages=['pkg.bar', 'pkg.foo'], + py_modules=['bar_et', 'bazish'], + ext_modules=[Extension('bar.ext', ['bar.c'])] + ) + + def testDefaults(self): + assert not Feature( + "test", standard=True, remove='x', available=False + ).include_by_default() + assert Feature("test", standard=True, remove='x').include_by_default() + # Feature must have either kwargs, removes, or require_features + with pytest.raises(DistutilsSetupError): + Feature("test") + + def testAvailability(self): + with pytest.raises(DistutilsPlatformError): + self.dist.features['dwim'].include_in(self.dist) + + def testFeatureOptions(self): + dist = self.dist + assert ( + ('with-dwim', None, 'include DWIM') in dist.feature_options + ) + assert ( + ('without-dwim', None, 'exclude DWIM (default)') in dist.feature_options + ) + assert ( + ('with-bar', None, 'include bar (default)') in dist.feature_options + ) + assert ( + ('without-bar', None, 'exclude bar') in dist.feature_options + ) + assert dist.feature_negopt['without-foo'] == 'with-foo' + assert dist.feature_negopt['without-bar'] == 'with-bar' + assert dist.feature_negopt['without-dwim'] == 'with-dwim' + assert ('without-baz' not in dist.feature_negopt) + + def testUseFeatures(self): + dist = self.dist + assert dist.with_foo == 1 + assert dist.with_bar == 0 + assert dist.with_baz == 1 + assert ('bar_et' not in dist.py_modules) + assert ('pkg.bar' not in dist.packages) + assert ('pkg.baz' in dist.packages) + assert ('scripts/baz_it' in dist.scripts) + assert (('libfoo', 'foo/foofoo.c') in dist.libraries) + assert dist.ext_modules == [] + assert dist.require_features == [self.req] + + # If we ask for bar, it should fail because we explicitly disabled + # it on the command line + with pytest.raises(DistutilsOptionError): + dist.include_feature('bar') + + def testFeatureWithInvalidRemove(self): + with pytest.raises(SystemExit): + makeSetup(features={'x': Feature('x', remove='y')}) + + +class TestCommandTests: + def testTestIsCommand(self): + test_cmd = makeSetup().get_command_obj('test') + assert (isinstance(test_cmd, distutils.cmd.Command)) + + def testLongOptSuiteWNoDefault(self): + ts1 = makeSetup(script_args=['test', '--test-suite=foo.tests.suite']) + ts1 = ts1.get_command_obj('test') + ts1.ensure_finalized() + assert ts1.test_suite == 'foo.tests.suite' + + def testDefaultSuite(self): + ts2 = makeSetup(test_suite='bar.tests.suite').get_command_obj('test') + ts2.ensure_finalized() + assert ts2.test_suite == 'bar.tests.suite' + + def testDefaultWModuleOnCmdLine(self): + ts3 = makeSetup( + test_suite='bar.tests', + script_args=['test', '-m', 'foo.tests'] + ).get_command_obj('test') + ts3.ensure_finalized() + assert ts3.test_module == 'foo.tests' + assert ts3.test_suite == 'foo.tests.test_suite' + + def testConflictingOptions(self): + ts4 = makeSetup( + script_args=['test', '-m', 'bar.tests', '-s', 'foo.tests.suite'] + ).get_command_obj('test') + with pytest.raises(DistutilsOptionError): + ts4.ensure_finalized() + + def testNoSuite(self): + ts5 = makeSetup().get_command_obj('test') + ts5.ensure_finalized() + assert ts5.test_suite is None @pytest.fixture diff --git a/setuptools/tests/test_test.py b/setuptools/tests/test_test.py index 7ea43c57..960527bc 100644 --- a/setuptools/tests/test_test.py +++ b/setuptools/tests/test_test.py @@ -2,9 +2,9 @@ from __future__ import unicode_literals +from distutils import log import os -import site -from distutils.errors import DistutilsError +import sys import pytest @@ -66,26 +66,66 @@ def sample_test(tmpdir_cwd): f.write(TEST_PY) -@pytest.mark.skipif('hasattr(sys, "real_prefix")') -@pytest.mark.usefixtures('user_override') -@pytest.mark.usefixtures('sample_test') -class TestTestTest: - def test_test(self): - params = dict( - name='foo', - packages=['name', 'name.space', 'name.space.tests'], - namespace_packages=['name'], - test_suite='name.space.tests.test_suite', - use_2to3=True, - ) - dist = Distribution(params) - dist.script_name = 'setup.py' - cmd = test(dist) - cmd.user = 1 - cmd.ensure_finalized() - cmd.install_dir = site.USER_SITE - cmd.user = 1 - with contexts.quiet(): - # The test runner calls sys.exit - with contexts.suppress_exceptions(SystemExit): - cmd.run() +@pytest.fixture +def quiet_log(): + # Running some of the other tests will automatically + # change the log level to info, messing our output. + log.set_verbosity(0) + + +@pytest.mark.usefixtures('sample_test', 'quiet_log') +def test_test(capfd): + params = dict( + name='foo', + packages=['name', 'name.space', 'name.space.tests'], + namespace_packages=['name'], + test_suite='name.space.tests.test_suite', + use_2to3=True, + ) + dist = Distribution(params) + dist.script_name = 'setup.py' + cmd = test(dist) + cmd.ensure_finalized() + # The test runner calls sys.exit + with contexts.suppress_exceptions(SystemExit): + cmd.run() + out, err = capfd.readouterr() + assert out == 'Foo\n' + + +@pytest.mark.xfail( + sys.version_info < (2, 7), + reason="No discover support for unittest on Python 2.6", +) +@pytest.mark.usefixtures('tmpdir_cwd', 'quiet_log') +def test_tests_are_run_once(capfd): + params = dict( + name='foo', + packages=['dummy'], + ) + with open('setup.py', 'wt') as f: + f.write('from setuptools import setup; setup(\n') + for k, v in sorted(params.items()): + f.write(' %s=%r,\n' % (k, v)) + f.write(')\n') + os.makedirs('dummy') + with open('dummy/__init__.py', 'wt'): + pass + with open('dummy/test_dummy.py', 'wt') as f: + f.write(DALS( + """ + from __future__ import print_function + import unittest + class TestTest(unittest.TestCase): + def test_test(self): + print('Foo') + """)) + dist = Distribution(params) + dist.script_name = 'setup.py' + cmd = test(dist) + cmd.ensure_finalized() + # The test runner calls sys.exit + with contexts.suppress_exceptions(SystemExit): + cmd.run() + out, err = capfd.readouterr() + assert out == 'Foo\n' diff --git a/setuptools/tests/test_virtualenv.py b/setuptools/tests/test_virtualenv.py index 17b8793c..9dbd3c86 100644 --- a/setuptools/tests/test_virtualenv.py +++ b/setuptools/tests/test_virtualenv.py @@ -1,5 +1,6 @@ import glob import os +import sys from pytest import yield_fixture from pytest_fixture_config import yield_requires_config @@ -39,6 +40,9 @@ def test_pip_upgrade_from_source(virtualenv): Check pip can upgrade setuptools from source. """ dist_dir = virtualenv.workspace + if sys.version_info < (2, 7): + # Python 2.6 support was dropped in wheel 0.30.0. + virtualenv.run('pip install -U "wheel<0.30.0"') # Generate source distribution / wheel. virtualenv.run(' && '.join(( 'cd {source}', diff --git a/setuptools/tests/text.py b/setuptools/tests/text.py new file mode 100644 index 00000000..ad2c6249 --- /dev/null +++ b/setuptools/tests/text.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- + +from __future__ import unicode_literals + + +class Filenames: + unicode = 'smörbröd.py' + latin_1 = unicode.encode('latin-1') + utf_8 = unicode.encode('utf-8') |
