# -*- coding: utf-8 -*- """Easy install Tests """ from __future__ import absolute_import import sys import os import tempfile import site import contextlib import tarfile import logging import itertools import distutils.errors import io import zipfile 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 import setuptools.command.easy_install as ei from setuptools.command.easy_install import PthDistributions from setuptools.command import easy_install as easy_install_pkg from setuptools.dist import Distribution from pkg_resources import normalize_path, working_set from pkg_resources import Distribution as PRDistribution import setuptools.tests.server import pkg_resources from .py26compat import tarfile_open from . import contexts from .textwrap import DALS class FakeDist(object): def get_entry_map(self, group): if group != 'console_scripts': return {} return {'name': 'ep'} def as_requirement(self): return 'spec' SETUP_PY = DALS(""" from setuptools import setup setup(name='foo') """) class TestEasyInstallTest: def test_install_site_py(self, tmpdir): dist = Distribution() cmd = ei.easy_install(dist) cmd.sitepy_installed = False cmd.install_dir = str(tmpdir) cmd.install_site_py() assert (tmpdir / 'site.py').exists() def test_get_script_args(self): header = ei.CommandSpec.best().from_environment().as_header() expected = header + DALS(""" # EASY-INSTALL-ENTRY-SCRIPT: 'spec','console_scripts','name' __requires__ = 'spec' import re import sys from pkg_resources import load_entry_point if __name__ == '__main__': sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]) sys.exit( load_entry_point('spec', 'console_scripts', 'name')() ) """) dist = FakeDist() args = next(ei.ScriptWriter.get_args(dist)) name, script = itertools.islice(args, 2) assert script == expected def test_no_find_links(self): # new option '--no-find-links', that blocks find-links added at # the project level dist = Distribution() cmd = ei.easy_install(dist) cmd.check_pth_processing = lambda: True cmd.no_find_links = True cmd.find_links = ['link1', 'link2'] cmd.install_dir = os.path.join(tempfile.mkdtemp(), 'ok') cmd.args = ['ok'] cmd.ensure_finalized() assert cmd.package_index.scanned_urls == {} # let's try without it (default behavior) cmd = ei.easy_install(dist) cmd.check_pth_processing = lambda: True cmd.find_links = ['link1', 'link2'] cmd.install_dir = os.path.join(tempfile.mkdtemp(), 'ok') cmd.args = ['ok'] cmd.ensure_finalized() keys = sorted(cmd.package_index.scanned_urls.keys()) assert keys == ['link1', 'link2'] def test_write_exception(self): """ Test that `cant_write_to_target` is rendered as a DistutilsError. """ dist = Distribution() cmd = ei.easy_install(dist) cmd.install_dir = os.getcwd() with pytest.raises(distutils.errors.DistutilsError): cmd.cant_write_to_target() def test_all_site_dirs(self, monkeypatch): """ get_site_dirs should always return site dirs reported by site.getsitepackages. """ path = normalize_path('/setuptools/test/site-packages') mock_gsp = lambda: [path] monkeypatch.setattr(site, 'getsitepackages', mock_gsp, raising=False) assert path in ei.get_site_dirs() def test_all_site_dirs_works_without_getsitepackages(self, monkeypatch): monkeypatch.delattr(site, 'getsitepackages', raising=False) assert ei.get_site_dirs() @pytest.fixture def sdist_unicode(self, tmpdir): files = [ ( 'setup.py', DALS(""" import setuptools setuptools.setup( name="setuptools-test-unicode", version="1.0", packages=["mypkg"], include_package_data=True, ) """), ), ( 'mypkg/__init__.py', "", ), ( u'mypkg/\u2603.txt', "", ), ] sdist_name = 'setuptools-test-unicode-1.0.zip' sdist = tmpdir / sdist_name # can't use make_sdist, because the issue only occurs # with zip sdists. sdist_zip = zipfile.ZipFile(str(sdist), 'w') for filename, content in files: sdist_zip.writestr(filename, content) 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") def test_unicode_filename_in_sdist(self, sdist_unicode, tmpdir, monkeypatch): """ The install command should execute correctly even if the package has unicode filenames. """ dist = Distribution({'script_args': ['easy_install']}) target = (tmpdir / 'target').ensure_dir() cmd = ei.easy_install( dist, install_dir=str(target), args=['x'], ) monkeypatch.setitem(os.environ, 'PYTHONPATH', str(target)) cmd.ensure_finalized() cmd.easy_install(sdist_unicode) class TestPTHFileWriter: def test_add_from_cwd_site_sets_dirty(self): '''a pth file manager should set dirty if a distribution is in site but also the cwd ''' pth = PthDistributions('does-not_exist', [os.getcwd()]) assert not pth.dirty pth.add(PRDistribution(os.getcwd())) assert pth.dirty def test_add_from_site_is_ignored(self): location = '/test/location/does-not-have-to-exist' # PthDistributions expects all locations to be normalized location = pkg_resources.normalize_path(location) pth = PthDistributions('does-not_exist', [location, ]) assert not pth.dirty pth.add(PRDistribution(location)) assert not pth.dirty @pytest.yield_fixture def setup_context(tmpdir): with (tmpdir / 'setup.py').open('w') as f: f.write(SETUP_PY) with tmpdir.as_cwd(): yield tmpdir @pytest.mark.usefixtures("user_override") @pytest.mark.usefixtures("setup_context") class TestUserInstallTest: # prevent check that site-packages is writable. easy_install # shouldn't be writing to system site-packages during finalize # options, but while it does, bypass the behavior. prev_sp_write = mock.patch( 'setuptools.command.easy_install.easy_install.check_site_dir', mock.Mock(), ) # simulate setuptools installed in user site packages @mock.patch('setuptools.command.easy_install.__file__', site.USER_SITE) @mock.patch('site.ENABLE_USER_SITE', True) @prev_sp_write def test_user_install_not_implied_user_site_enabled(self): self.assert_not_user_site() @mock.patch('site.ENABLE_USER_SITE', False) @prev_sp_write def test_user_install_not_implied_user_site_disabled(self): self.assert_not_user_site() @staticmethod def assert_not_user_site(): # create a finalized easy_install command dist = Distribution() dist.script_name = 'setup.py' cmd = ei.easy_install(dist) cmd.args = ['py'] cmd.ensure_finalized() assert not cmd.user, 'user should not be implied' def test_multiproc_atexit(self): pytest.importorskip('multiprocessing') log = logging.getLogger('test_easy_install') logging.basicConfig(level=logging.INFO, stream=sys.stderr) log.info('this should not break') @pytest.fixture() def foo_package(self, tmpdir): egg_file = tmpdir / 'foo-1.0.egg-info' with egg_file.open('w') as f: f.write('Name: foo\n') return str(tmpdir) @pytest.yield_fixture() def install_target(self, tmpdir): target = str(tmpdir) with mock.patch('sys.path', sys.path + [target]): python_path = os.path.pathsep.join(sys.path) with mock.patch.dict(os.environ, PYTHONPATH=python_path): yield target def test_local_index(self, foo_package, install_target): """ The local index must be used when easy_install locates installed packages. """ dist = Distribution() dist.script_name = 'setup.py' cmd = ei.easy_install(dist) cmd.install_dir = install_target cmd.args = ['foo'] cmd.ensure_finalized() cmd.local_index.scan([foo_package]) res = cmd.easy_install('foo') actual = os.path.normcase(os.path.realpath(res.location)) expected = os.path.normcase(os.path.realpath(foo_package)) assert actual == expected @contextlib.contextmanager def user_install_setup_context(self, *args, **kwargs): """ Wrap sandbox.setup_context to patch easy_install in that context to appear as user-installed. """ with self.orig_context(*args, **kwargs): import setuptools.command.easy_install as ei ei.__file__ = site.USER_SITE yield def patched_setup_context(self): self.orig_context = sandbox.setup_context return mock.patch( 'setuptools.sandbox.setup_context', self.user_install_setup_context, ) @pytest.yield_fixture def distutils_package(): distutils_setup_py = SETUP_PY.replace( 'from setuptools import setup', 'from distutils.core import setup', ) with contexts.tempdir(cd=os.chdir): with open('setup.py', 'w') as f: f.write(distutils_setup_py) yield class TestDistutilsPackage: def test_bdist_egg_available_on_distutils_pkg(self, distutils_package): run_setup('setup.py', ['bdist_egg']) class TestSetupRequires: def test_setup_requires_honors_fetch_params(self): """ When easy_install installs a source distribution which specifies setup_requires, it should honor the fetch parameters (such as allow-hosts, index-url, and find-links). """ # set up a server which will simulate an alternate package index. p_index = setuptools.tests.server.MockServer() p_index.start() netloc = 1 p_index_loc = urllib.parse.urlparse(p_index.url)[netloc] if p_index_loc.endswith(':0'): # Some platforms (Jython) don't find a port to which to bind, # so skip this test for them. return with contexts.quiet(): # create an sdist that has a build-time dependency. with TestSetupRequires.create_sdist() as dist_file: with contexts.tempdir() as temp_install_dir: with contexts.environment(PYTHONPATH=temp_install_dir): ei_params = [ '--index-url', p_index.url, '--allow-hosts', p_index_loc, '--exclude-scripts', '--install-dir', temp_install_dir, dist_file, ] with sandbox.save_argv(['easy_install']): # attempt to install the dist. It should fail because # it doesn't exist. with pytest.raises(SystemExit): easy_install_pkg.main(ei_params) # there should have been two or three requests to the server # (three happens on Python 3.3a) assert 2 <= len(p_index.requests) <= 3 assert p_index.requests[0].path == '/does-not-exist/' @staticmethod @contextlib.contextmanager def create_sdist(): """ Return an sdist with a setup_requires dependency (of something that doesn't exist) """ with contexts.tempdir() as dir: dist_path = os.path.join(dir, 'setuptools-test-fetcher-1.0.tar.gz') make_sdist(dist_path, [ ('setup.py', DALS(""" import setuptools setuptools.setup( name="setuptools-test-fetcher", version="1.0", setup_requires = ['does-not-exist'], ) """))]) yield dist_path def test_setup_requires_overrides_version_conflict(self): """ Regression test for distribution issue 323: https://bitbucket.org/tarek/distribute/issues/323 Ensures that a distribution's setup_requires requirements can still be installed and used locally even if a conflicting version of that requirement is already on the path. """ fake_dist = PRDistribution('does-not-matter', project_name='foobar', version='0.0') working_set.add(fake_dist) with contexts.save_pkg_resources_state(): with contexts.tempdir() as temp_dir: test_pkg = create_setup_requires_package(temp_dir) 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 # running the setup.py at all is sufficient run_setup(test_setup_py, ['--name']) lines = stdout.readlines() assert len(lines) > 0 assert lines[-1].strip(), 'test_pkg' def test_setup_requires_override_nspkg(self): """ Like ``test_setup_requires_overrides_version_conflict`` but where the ``setup_requires`` package is part of a namespace package that has *already* been imported. """ with contexts.save_pkg_resources_state(): with contexts.tempdir() as temp_dir: foobar_1_archive = os.path.join(temp_dir, 'foo.bar-0.1.tar.gz') make_nspkg_sdist(foobar_1_archive, 'foo.bar', '0.1') # Now actually go ahead an extract to the temp dir and add the # extracted path to sys.path so foo.bar v0.1 is importable foobar_1_dir = os.path.join(temp_dir, 'foo.bar-0.1') os.mkdir(foobar_1_dir) with tarfile_open(foobar_1_archive) as tf: tf.extractall(foobar_1_dir) sys.path.insert(1, foobar_1_dir) dist = PRDistribution(foobar_1_dir, project_name='foo.bar', version='0.1') working_set.add(dist) template = DALS("""\ import foo # Even with foo imported first the # setup_requires package should override import setuptools setuptools.setup(**%r) if not (hasattr(foo, '__path__') and len(foo.__path__) == 2): print('FAIL') if 'foo.bar-0.2' not in foo.__path__[0]: print('FAIL') """) test_pkg = create_setup_requires_package( temp_dir, 'foo.bar', '0.2', make_nspkg_sdist, template) test_setup_py = os.path.join(test_pkg, 'setup.py') with contexts.quiet() as (stdout, stderr): try: # Don't even need to install the package, just # running the setup.py at all is sufficient run_setup(test_setup_py, ['--name']) except pkg_resources.VersionConflict: self.fail('Installing setup.py requirements ' 'caused a VersionConflict') assert 'FAIL' not in stdout.getvalue() lines = stdout.readlines() assert len(lines) > 0 assert lines[-1].strip() == 'test_pkg' def make_trivial_sdist(dist_path, distname, version): """ Create a simple sdist tarball at dist_path, containing just a simple setup.py. """ make_sdist(dist_path, [ ('setup.py', DALS("""\ import setuptools setuptools.setup( name=%r, version=%r ) """ % (distname, version)))]) def make_nspkg_sdist(dist_path, distname, version): """ Make an sdist tarball with distname and version which also contains one package with the same name as distname. The top-level package is designated a namespace package). """ parts = distname.split('.') nspackage = parts[0] packages = ['.'.join(parts[:idx]) for idx in range(1, len(parts) + 1)] setup_py = DALS("""\ import setuptools setuptools.setup( name=%r, version=%r, packages=%r, namespace_packages=[%r] ) """ % (distname, version, packages, nspackage)) init = "__import__('pkg_resources').declare_namespace(__name__)" files = [('setup.py', setup_py), (os.path.join(nspackage, '__init__.py'), init)] for package in packages[1:]: filename = os.path.join(*(package.split('.') + ['__init__.py'])) files.append((filename, '')) make_sdist(dist_path, files) def make_sdist(dist_path, files): """ Create a simple sdist tarball at dist_path, containing the files listed in ``files`` as ``(filename, content)`` tuples. """ with tarfile_open(dist_path, 'w:gz') as dist: for filename, content in files: file_bytes = io.BytesIO(content.encode('utf-8')) file_info = tarfile.TarInfo(name=filename) file_info.size = len(file_bytes.getvalue()) file_info.mtime = int(time.time()) dist.addfile(file_info, fileobj=file_bytes) def create_setup_requires_package(path, distname='foobar', version='0.1', make_package=make_trivial_sdist, setup_py_template=None): """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. ``distname`` and ``version`` refer to the name/version of the package that the test package requires via ``setup_requires``. The name of the test package itself is just 'test_pkg'. """ test_setup_attrs = { 'name': 'test_pkg', 'version': '0.0', 'setup_requires': ['%s==%s' % (distname, version)], 'dependency_links': [os.path.abspath(path)] } test_pkg = os.path.join(path, 'test_pkg') test_setup_py = os.path.join(test_pkg, 'setup.py') os.mkdir(test_pkg) if setup_py_template is None: setup_py_template = DALS("""\ import setuptools setuptools.setup(**%r) """) with open(test_setup_py, 'w') as f: f.write(setup_py_template % test_setup_attrs) foobar_path = os.path.join(path, '%s-%s.tar.gz' % (distname, version)) make_package(foobar_path, distname, version) 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" ) class TestScriptHeader: non_ascii_exe = '/Users/José/bin/python' exe_with_spaces = r'C:\Program Files\Python33\python.exe' def test_get_script_header(self): expected = '#!%s\n' % ei.nt_quote_arg(os.path.normpath(sys.executable)) actual = ei.ScriptWriter.get_script_header('#!/usr/local/bin/python') assert actual == expected def test_get_script_header_args(self): expected = '#!%s -x\n' % ei.nt_quote_arg(os.path.normpath (sys.executable)) actual = ei.ScriptWriter.get_script_header('#!/usr/bin/python -x') assert actual == expected def test_get_script_header_non_ascii_exe(self): actual = ei.ScriptWriter.get_script_header('#!/usr/bin/python', executable=self.non_ascii_exe) expected = '#!%s -x\n' % self.non_ascii_exe assert actual == expected def test_get_script_header_exe_with_spaces(self): actual = ei.ScriptWriter.get_script_header('#!/usr/bin/python', executable='"' + self.exe_with_spaces + '"') expected = '#!"%s"\n' % self.exe_with_spaces assert actual == expected class TestCommandSpec: def test_custom_launch_command(self): """ Show how a custom CommandSpec could be used to specify a #! executable which takes parameters. """ cmd = ei.CommandSpec(['/usr/bin/env', 'python3']) assert cmd.as_header() == '#!/usr/bin/env python3\n' def test_from_param_for_CommandSpec_is_passthrough(self): """ from_param should return an instance of a CommandSpec """ cmd = ei.CommandSpec(['python']) cmd_new = ei.CommandSpec.from_param(cmd) assert cmd is cmd_new @mock.patch('sys.executable', TestScriptHeader.exe_with_spaces) @mock.patch.dict(os.environ) def test_from_environment_with_spaces_in_executable(self): os.environ.pop('__PYVENV_LAUNCHER__', None) cmd = ei.CommandSpec.from_environment() assert len(cmd) == 1 assert cmd.as_header().startswith('#!"') def test_from_simple_string_uses_shlex(self): """ In order to support `executable = /usr/bin/env my-python`, make sure from_param invokes shlex on that input. """ cmd = ei.CommandSpec.from_param('/usr/bin/env my-python') assert len(cmd) == 2 assert '"' not in cmd.as_header() class TestWindowsScriptWriter: def test_header(self): hdr = ei.WindowsScriptWriter.get_script_header('') assert hdr.startswith('#!') assert hdr.endswith('\n') hdr = hdr.lstrip('#!') hdr = hdr.rstrip('\n') # header should not start with an escaped quote assert not hdr.startswith('\\"')