# -*- coding: utf-8 -*- """Easy install Tests """ from __future__ import absolute_import, unicode_literals import sys import os import tempfile import site import contextlib import tarfile import logging import itertools import distutils.errors import io import zipfile import mock from setuptools.command.easy_install import ( EasyInstallDeprecationWarning, ScriptWriter, WindowsScriptWriter, ) import time from setuptools.extern import six from setuptools.extern.six.moves import urllib import pytest 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 from setuptools.tests import fail_on_ascii import pkg_resources from . import contexts from .textwrap import DALS __metaclass__ = type class FakeDist: def get_entry_map(self, group): if group != 'console_scripts': return {} return {str('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(r""" # 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')() ) """) # noqa: E501 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') def mock_gsp(): return [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', "", ), ( 'mypkg/☃.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) @fail_on_ascii 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) @pytest.fixture def sdist_unicode_in_script(self, tmpdir): files = [ ( "setup.py", DALS(""" import setuptools setuptools.setup( name="setuptools-test-unicode", version="1.0", packages=["mypkg"], include_package_data=True, scripts=['mypkg/unicode_in_script'], ) """), ), ("mypkg/__init__.py", ""), ( "mypkg/unicode_in_script", DALS( """ #!/bin/sh # á non_python_fn() { } """), ), ] sdist_name = "setuptools-test-unicode-script-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.encode('utf-8')) sdist_zip.close() return str(sdist) @fail_on_ascii def test_unicode_content_in_sdist( self, sdist_unicode_in_script, tmpdir, monkeypatch): """ The install command should execute correctly even if the package has unicode in scripts. """ 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_in_script) @pytest.fixture def sdist_script(self, tmpdir): files = [ ( 'setup.py', DALS(""" import setuptools setuptools.setup( name="setuptools-test-script", version="1.0", scripts=["mypkg_script"], ) """), ), ( 'mypkg_script', DALS(""" #/usr/bin/python print('mypkg_script') """), ), ] sdist_name = 'setuptools-test-script-1.0.zip' sdist = str(tmpdir / sdist_name) make_sdist(sdist, files) return sdist @pytest.mark.skipif(not sys.platform.startswith('linux'), reason="Test can only be run on Linux") def test_script_install(self, sdist_script, tmpdir, monkeypatch): """ Check scripts are installed. """ 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_script) assert (target / 'mypkg_script').exists() def test_dist_get_script_args_deprecated(self): with pytest.warns(EasyInstallDeprecationWarning): ScriptWriter.get_script_args(None, None) def test_dist_get_script_header_deprecated(self): with pytest.warns(EasyInstallDeprecationWarning): ScriptWriter.get_script_header("") def test_dist_get_writer_deprecated(self): with pytest.warns(EasyInstallDeprecationWarning): ScriptWriter.get_writer(None) def test_dist_WindowsScriptWriter_get_writer_deprecated(self): with pytest.warns(EasyInstallDeprecationWarning): WindowsScriptWriter.get_writer() @pytest.mark.filterwarnings('ignore:Unbuilt egg') 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 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 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, 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 # running the setup.py at all is sufficient run_setup(test_setup_py, [str('--name')]) lines = stdout.readlines() assert len(lines) > 0 assert lines[-1].strip() == 'test_pkg' @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 *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, use_setup_cfg=use_setup_cfg) 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, [str('--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' @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): files = [( '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 """), )] make_sdist(dist_path, files) 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, [str('--version')]) lines = stdout.readlines() assert len(lines) > 0 assert lines[-1].strip() == '42' 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, 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. ``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_setup_attrs.update(setup_attrs) test_pkg = os.path.join(path, 'test_pkg') 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 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 @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' if six.PY2: non_ascii_exe = non_ascii_exe.encode('utf-8') exe_with_spaces = r'C:\Program Files\Python36\python.exe' def test_get_script_header(self): expected = '#!%s\n' % ei.nt_quote_arg(os.path.normpath(sys.executable)) actual = ei.ScriptWriter.get_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_header('#!/usr/bin/python -x') assert actual == expected def test_get_script_header_non_ascii_exe(self): actual = ei.ScriptWriter.get_header( '#!/usr/bin/python', executable=self.non_ascii_exe) expected = str('#!%s -x\n') % self.non_ascii_exe assert actual == expected def test_get_script_header_exe_with_spaces(self): actual = ei.ScriptWriter.get_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_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('\\"')