1"""Easy install Tests
2"""
3
4import sys
5import os
6import tempfile
7import site
8import contextlib
9import tarfile
10import logging
11import itertools
12import distutils.errors
13import io
14import zipfile
15import mock
16import time
17import re
18import subprocess
19import pathlib
20import warnings
21from collections import namedtuple
22
23import pytest
24from jaraco import path
25
26from setuptools import sandbox
27from setuptools.sandbox import run_setup
28import setuptools.command.easy_install as ei
29from setuptools.command.easy_install import (
30    EasyInstallDeprecationWarning, ScriptWriter, PthDistributions,
31    WindowsScriptWriter,
32)
33from setuptools.dist import Distribution
34from pkg_resources import normalize_path, working_set
35from pkg_resources import Distribution as PRDistribution
36from setuptools.tests.server import MockServer, path_to_url
37from setuptools.tests import fail_on_ascii
38import pkg_resources
39
40from . import contexts
41from .textwrap import DALS
42
43
44@pytest.fixture(autouse=True)
45def pip_disable_index(monkeypatch):
46    """
47    Important: Disable the default index for pip to avoid
48    querying packages in the index and potentially resolving
49    and installing packages there.
50    """
51    monkeypatch.setenv('PIP_NO_INDEX', 'true')
52
53
54class FakeDist:
55    def get_entry_map(self, group):
56        if group != 'console_scripts':
57            return {}
58        return {str('name'): 'ep'}
59
60    def as_requirement(self):
61        return 'spec'
62
63
64SETUP_PY = DALS("""
65    from setuptools import setup
66
67    setup()
68    """)
69
70
71class TestEasyInstallTest:
72    def test_get_script_args(self):
73        header = ei.CommandSpec.best().from_environment().as_header()
74        dist = FakeDist()
75        args = next(ei.ScriptWriter.get_args(dist))
76        name, script = itertools.islice(args, 2)
77        assert script.startswith(header)
78        assert "'spec'" in script
79        assert "'console_scripts'" in script
80        assert "'name'" in script
81        assert re.search(
82            '^# EASY-INSTALL-ENTRY-SCRIPT', script, flags=re.MULTILINE)
83
84    def test_no_find_links(self):
85        # new option '--no-find-links', that blocks find-links added at
86        # the project level
87        dist = Distribution()
88        cmd = ei.easy_install(dist)
89        cmd.check_pth_processing = lambda: True
90        cmd.no_find_links = True
91        cmd.find_links = ['link1', 'link2']
92        cmd.install_dir = os.path.join(tempfile.mkdtemp(), 'ok')
93        cmd.args = ['ok']
94        cmd.ensure_finalized()
95        assert cmd.package_index.scanned_urls == {}
96
97        # let's try without it (default behavior)
98        cmd = ei.easy_install(dist)
99        cmd.check_pth_processing = lambda: True
100        cmd.find_links = ['link1', 'link2']
101        cmd.install_dir = os.path.join(tempfile.mkdtemp(), 'ok')
102        cmd.args = ['ok']
103        cmd.ensure_finalized()
104        keys = sorted(cmd.package_index.scanned_urls.keys())
105        assert keys == ['link1', 'link2']
106
107    def test_write_exception(self):
108        """
109        Test that `cant_write_to_target` is rendered as a DistutilsError.
110        """
111        dist = Distribution()
112        cmd = ei.easy_install(dist)
113        cmd.install_dir = os.getcwd()
114        with pytest.raises(distutils.errors.DistutilsError):
115            cmd.cant_write_to_target()
116
117    def test_all_site_dirs(self, monkeypatch):
118        """
119        get_site_dirs should always return site dirs reported by
120        site.getsitepackages.
121        """
122        path = normalize_path('/setuptools/test/site-packages')
123
124        def mock_gsp():
125            return [path]
126        monkeypatch.setattr(site, 'getsitepackages', mock_gsp, raising=False)
127        assert path in ei.get_site_dirs()
128
129    def test_all_site_dirs_works_without_getsitepackages(self, monkeypatch):
130        monkeypatch.delattr(site, 'getsitepackages', raising=False)
131        assert ei.get_site_dirs()
132
133    @pytest.fixture
134    def sdist_unicode(self, tmpdir):
135        files = [
136            (
137                'setup.py',
138                DALS("""
139                    import setuptools
140                    setuptools.setup(
141                        name="setuptools-test-unicode",
142                        version="1.0",
143                        packages=["mypkg"],
144                        include_package_data=True,
145                    )
146                    """),
147            ),
148            (
149                'mypkg/__init__.py',
150                "",
151            ),
152            (
153                'mypkg/☃.txt',
154                "",
155            ),
156        ]
157        sdist_name = 'setuptools-test-unicode-1.0.zip'
158        sdist = tmpdir / sdist_name
159        # can't use make_sdist, because the issue only occurs
160        #  with zip sdists.
161        sdist_zip = zipfile.ZipFile(str(sdist), 'w')
162        for filename, content in files:
163            sdist_zip.writestr(filename, content)
164        sdist_zip.close()
165        return str(sdist)
166
167    @fail_on_ascii
168    def test_unicode_filename_in_sdist(
169            self, sdist_unicode, tmpdir, monkeypatch):
170        """
171        The install command should execute correctly even if
172        the package has unicode filenames.
173        """
174        dist = Distribution({'script_args': ['easy_install']})
175        target = (tmpdir / 'target').ensure_dir()
176        cmd = ei.easy_install(
177            dist,
178            install_dir=str(target),
179            args=['x'],
180        )
181        monkeypatch.setitem(os.environ, 'PYTHONPATH', str(target))
182        cmd.ensure_finalized()
183        cmd.easy_install(sdist_unicode)
184
185    @pytest.fixture
186    def sdist_unicode_in_script(self, tmpdir):
187        files = [
188            (
189                "setup.py",
190                DALS("""
191                    import setuptools
192                    setuptools.setup(
193                        name="setuptools-test-unicode",
194                        version="1.0",
195                        packages=["mypkg"],
196                        include_package_data=True,
197                        scripts=['mypkg/unicode_in_script'],
198                    )
199                    """),
200            ),
201            ("mypkg/__init__.py", ""),
202            (
203                "mypkg/unicode_in_script",
204                DALS(
205                    """
206                    #!/bin/sh
207                    # á
208
209                    non_python_fn() {
210                    }
211                """),
212            ),
213        ]
214        sdist_name = "setuptools-test-unicode-script-1.0.zip"
215        sdist = tmpdir / sdist_name
216        # can't use make_sdist, because the issue only occurs
217        #  with zip sdists.
218        sdist_zip = zipfile.ZipFile(str(sdist), "w")
219        for filename, content in files:
220            sdist_zip.writestr(filename, content.encode('utf-8'))
221        sdist_zip.close()
222        return str(sdist)
223
224    @fail_on_ascii
225    def test_unicode_content_in_sdist(
226            self, sdist_unicode_in_script, tmpdir, monkeypatch):
227        """
228        The install command should execute correctly even if
229        the package has unicode in scripts.
230        """
231        dist = Distribution({"script_args": ["easy_install"]})
232        target = (tmpdir / "target").ensure_dir()
233        cmd = ei.easy_install(dist, install_dir=str(target), args=["x"])
234        monkeypatch.setitem(os.environ, "PYTHONPATH", str(target))
235        cmd.ensure_finalized()
236        cmd.easy_install(sdist_unicode_in_script)
237
238    @pytest.fixture
239    def sdist_script(self, tmpdir):
240        files = [
241            (
242                'setup.py',
243                DALS("""
244                    import setuptools
245                    setuptools.setup(
246                        name="setuptools-test-script",
247                        version="1.0",
248                        scripts=["mypkg_script"],
249                    )
250                    """),
251            ),
252            (
253                'mypkg_script',
254                DALS("""
255                     #/usr/bin/python
256                     print('mypkg_script')
257                     """),
258            ),
259        ]
260        sdist_name = 'setuptools-test-script-1.0.zip'
261        sdist = str(tmpdir / sdist_name)
262        make_sdist(sdist, files)
263        return sdist
264
265    @pytest.mark.skipif(not sys.platform.startswith('linux'),
266                        reason="Test can only be run on Linux")
267    def test_script_install(self, sdist_script, tmpdir, monkeypatch):
268        """
269        Check scripts are installed.
270        """
271        dist = Distribution({'script_args': ['easy_install']})
272        target = (tmpdir / 'target').ensure_dir()
273        cmd = ei.easy_install(
274            dist,
275            install_dir=str(target),
276            args=['x'],
277        )
278        monkeypatch.setitem(os.environ, 'PYTHONPATH', str(target))
279        cmd.ensure_finalized()
280        cmd.easy_install(sdist_script)
281        assert (target / 'mypkg_script').exists()
282
283    def test_dist_get_script_args_deprecated(self):
284        with pytest.warns(EasyInstallDeprecationWarning):
285            ScriptWriter.get_script_args(None, None)
286
287    def test_dist_get_script_header_deprecated(self):
288        with pytest.warns(EasyInstallDeprecationWarning):
289            ScriptWriter.get_script_header("")
290
291    def test_dist_get_writer_deprecated(self):
292        with pytest.warns(EasyInstallDeprecationWarning):
293            ScriptWriter.get_writer(None)
294
295    def test_dist_WindowsScriptWriter_get_writer_deprecated(self):
296        with pytest.warns(EasyInstallDeprecationWarning):
297            WindowsScriptWriter.get_writer()
298
299
300@pytest.mark.filterwarnings('ignore:Unbuilt egg')
301class TestPTHFileWriter:
302    def test_add_from_cwd_site_sets_dirty(self):
303        '''a pth file manager should set dirty
304        if a distribution is in site but also the cwd
305        '''
306        pth = PthDistributions('does-not_exist', [os.getcwd()])
307        assert not pth.dirty
308        pth.add(PRDistribution(os.getcwd()))
309        assert pth.dirty
310
311    def test_add_from_site_is_ignored(self):
312        location = '/test/location/does-not-have-to-exist'
313        # PthDistributions expects all locations to be normalized
314        location = pkg_resources.normalize_path(location)
315        pth = PthDistributions('does-not_exist', [location, ])
316        assert not pth.dirty
317        pth.add(PRDistribution(location))
318        assert not pth.dirty
319
320
321@pytest.fixture
322def setup_context(tmpdir):
323    with (tmpdir / 'setup.py').open('w') as f:
324        f.write(SETUP_PY)
325    with tmpdir.as_cwd():
326        yield tmpdir
327
328
329@pytest.mark.usefixtures("user_override")
330@pytest.mark.usefixtures("setup_context")
331class TestUserInstallTest:
332
333    # prevent check that site-packages is writable. easy_install
334    # shouldn't be writing to system site-packages during finalize
335    # options, but while it does, bypass the behavior.
336    prev_sp_write = mock.patch(
337        'setuptools.command.easy_install.easy_install.check_site_dir',
338        mock.Mock(),
339    )
340
341    # simulate setuptools installed in user site packages
342    @mock.patch('setuptools.command.easy_install.__file__', site.USER_SITE)
343    @mock.patch('site.ENABLE_USER_SITE', True)
344    @prev_sp_write
345    def test_user_install_not_implied_user_site_enabled(self):
346        self.assert_not_user_site()
347
348    @mock.patch('site.ENABLE_USER_SITE', False)
349    @prev_sp_write
350    def test_user_install_not_implied_user_site_disabled(self):
351        self.assert_not_user_site()
352
353    @staticmethod
354    def assert_not_user_site():
355        # create a finalized easy_install command
356        dist = Distribution()
357        dist.script_name = 'setup.py'
358        cmd = ei.easy_install(dist)
359        cmd.args = ['py']
360        cmd.ensure_finalized()
361        assert not cmd.user, 'user should not be implied'
362
363    def test_multiproc_atexit(self):
364        pytest.importorskip('multiprocessing')
365
366        log = logging.getLogger('test_easy_install')
367        logging.basicConfig(level=logging.INFO, stream=sys.stderr)
368        log.info('this should not break')
369
370    @pytest.fixture()
371    def foo_package(self, tmpdir):
372        egg_file = tmpdir / 'foo-1.0.egg-info'
373        with egg_file.open('w') as f:
374            f.write('Name: foo\n')
375        return str(tmpdir)
376
377    @pytest.fixture()
378    def install_target(self, tmpdir):
379        target = str(tmpdir)
380        with mock.patch('sys.path', sys.path + [target]):
381            python_path = os.path.pathsep.join(sys.path)
382            with mock.patch.dict(os.environ, PYTHONPATH=python_path):
383                yield target
384
385    def test_local_index(self, foo_package, install_target):
386        """
387        The local index must be used when easy_install locates installed
388        packages.
389        """
390        dist = Distribution()
391        dist.script_name = 'setup.py'
392        cmd = ei.easy_install(dist)
393        cmd.install_dir = install_target
394        cmd.args = ['foo']
395        cmd.ensure_finalized()
396        cmd.local_index.scan([foo_package])
397        res = cmd.easy_install('foo')
398        actual = os.path.normcase(os.path.realpath(res.location))
399        expected = os.path.normcase(os.path.realpath(foo_package))
400        assert actual == expected
401
402    @contextlib.contextmanager
403    def user_install_setup_context(self, *args, **kwargs):
404        """
405        Wrap sandbox.setup_context to patch easy_install in that context to
406        appear as user-installed.
407        """
408        with self.orig_context(*args, **kwargs):
409            import setuptools.command.easy_install as ei
410            ei.__file__ = site.USER_SITE
411            yield
412
413    def patched_setup_context(self):
414        self.orig_context = sandbox.setup_context
415
416        return mock.patch(
417            'setuptools.sandbox.setup_context',
418            self.user_install_setup_context,
419        )
420
421
422@pytest.fixture
423def distutils_package():
424    distutils_setup_py = SETUP_PY.replace(
425        'from setuptools import setup',
426        'from distutils.core import setup',
427    )
428    with contexts.tempdir(cd=os.chdir):
429        with open('setup.py', 'w') as f:
430            f.write(distutils_setup_py)
431        yield
432
433
434@pytest.fixture
435def mock_index():
436    # set up a server which will simulate an alternate package index.
437    p_index = MockServer()
438    if p_index.server_port == 0:
439        # Some platforms (Jython) don't find a port to which to bind,
440        # so skip test for them.
441        pytest.skip("could not find a valid port")
442    p_index.start()
443    return p_index
444
445
446class TestDistutilsPackage:
447    def test_bdist_egg_available_on_distutils_pkg(self, distutils_package):
448        run_setup('setup.py', ['bdist_egg'])
449
450
451class TestSetupRequires:
452
453    def test_setup_requires_honors_fetch_params(self, mock_index, monkeypatch):
454        """
455        When easy_install installs a source distribution which specifies
456        setup_requires, it should honor the fetch parameters (such as
457        index-url, and find-links).
458        """
459        monkeypatch.setenv(str('PIP_RETRIES'), str('0'))
460        monkeypatch.setenv(str('PIP_TIMEOUT'), str('0'))
461        monkeypatch.setenv('PIP_NO_INDEX', 'false')
462        with contexts.quiet():
463            # create an sdist that has a build-time dependency.
464            with TestSetupRequires.create_sdist() as dist_file:
465                with contexts.tempdir() as temp_install_dir:
466                    with contexts.environment(PYTHONPATH=temp_install_dir):
467                        cmd = [
468                            sys.executable,
469                            '-m', 'setup',
470                            'easy_install',
471                            '--index-url', mock_index.url,
472                            '--exclude-scripts',
473                            '--install-dir', temp_install_dir,
474                            dist_file,
475                        ]
476                        subprocess.Popen(cmd).wait()
477        # there should have been one requests to the server
478        assert [r.path for r in mock_index.requests] == ['/does-not-exist/']
479
480    @staticmethod
481    @contextlib.contextmanager
482    def create_sdist():
483        """
484        Return an sdist with a setup_requires dependency (of something that
485        doesn't exist)
486        """
487        with contexts.tempdir() as dir:
488            dist_path = os.path.join(dir, 'setuptools-test-fetcher-1.0.tar.gz')
489            make_sdist(dist_path, [
490                ('setup.py', DALS("""
491                    import setuptools
492                    setuptools.setup(
493                        name="setuptools-test-fetcher",
494                        version="1.0",
495                        setup_requires = ['does-not-exist'],
496                    )
497                """)),
498                ('setup.cfg', ''),
499            ])
500            yield dist_path
501
502    use_setup_cfg = (
503        (),
504        ('dependency_links',),
505        ('setup_requires',),
506        ('dependency_links', 'setup_requires'),
507    )
508
509    @pytest.mark.parametrize('use_setup_cfg', use_setup_cfg)
510    def test_setup_requires_overrides_version_conflict(self, use_setup_cfg):
511        """
512        Regression test for distribution issue 323:
513        https://bitbucket.org/tarek/distribute/issues/323
514
515        Ensures that a distribution's setup_requires requirements can still be
516        installed and used locally even if a conflicting version of that
517        requirement is already on the path.
518        """
519
520        fake_dist = PRDistribution('does-not-matter', project_name='foobar',
521                                   version='0.0')
522        working_set.add(fake_dist)
523
524        with contexts.save_pkg_resources_state():
525            with contexts.tempdir() as temp_dir:
526                test_pkg = create_setup_requires_package(
527                    temp_dir, use_setup_cfg=use_setup_cfg)
528                test_setup_py = os.path.join(test_pkg, 'setup.py')
529                with contexts.quiet() as (stdout, stderr):
530                    # Don't even need to install the package, just
531                    # running the setup.py at all is sufficient
532                    run_setup(test_setup_py, [str('--name')])
533
534                lines = stdout.readlines()
535                assert len(lines) > 0
536                assert lines[-1].strip() == 'test_pkg'
537
538    @pytest.mark.parametrize('use_setup_cfg', use_setup_cfg)
539    def test_setup_requires_override_nspkg(self, use_setup_cfg):
540        """
541        Like ``test_setup_requires_overrides_version_conflict`` but where the
542        ``setup_requires`` package is part of a namespace package that has
543        *already* been imported.
544        """
545
546        with contexts.save_pkg_resources_state():
547            with contexts.tempdir() as temp_dir:
548                foobar_1_archive = os.path.join(temp_dir, 'foo.bar-0.1.tar.gz')
549                make_nspkg_sdist(foobar_1_archive, 'foo.bar', '0.1')
550                # Now actually go ahead an extract to the temp dir and add the
551                # extracted path to sys.path so foo.bar v0.1 is importable
552                foobar_1_dir = os.path.join(temp_dir, 'foo.bar-0.1')
553                os.mkdir(foobar_1_dir)
554                with tarfile.open(foobar_1_archive) as tf:
555                    tf.extractall(foobar_1_dir)
556                sys.path.insert(1, foobar_1_dir)
557
558                dist = PRDistribution(foobar_1_dir, project_name='foo.bar',
559                                      version='0.1')
560                working_set.add(dist)
561
562                template = DALS("""\
563                    import foo  # Even with foo imported first the
564                                # setup_requires package should override
565                    import setuptools
566                    setuptools.setup(**%r)
567
568                    if not (hasattr(foo, '__path__') and
569                            len(foo.__path__) == 2):
570                        print('FAIL')
571
572                    if 'foo.bar-0.2' not in foo.__path__[0]:
573                        print('FAIL')
574                """)
575
576                test_pkg = create_setup_requires_package(
577                    temp_dir, 'foo.bar', '0.2', make_nspkg_sdist, template,
578                    use_setup_cfg=use_setup_cfg)
579
580                test_setup_py = os.path.join(test_pkg, 'setup.py')
581
582                with contexts.quiet() as (stdout, stderr):
583                    try:
584                        # Don't even need to install the package, just
585                        # running the setup.py at all is sufficient
586                        run_setup(test_setup_py, [str('--name')])
587                    except pkg_resources.VersionConflict:
588                        self.fail(
589                            'Installing setup.py requirements '
590                            'caused a VersionConflict')
591
592                assert 'FAIL' not in stdout.getvalue()
593                lines = stdout.readlines()
594                assert len(lines) > 0
595                assert lines[-1].strip() == 'test_pkg'
596
597    @pytest.mark.parametrize('use_setup_cfg', use_setup_cfg)
598    def test_setup_requires_with_attr_version(self, use_setup_cfg):
599        def make_dependency_sdist(dist_path, distname, version):
600            files = [(
601                'setup.py',
602                DALS("""
603                    import setuptools
604                    setuptools.setup(
605                        name={name!r},
606                        version={version!r},
607                        py_modules=[{name!r}],
608                    )
609                    """.format(name=distname, version=version)),
610            ), (
611                distname + '.py',
612                DALS("""
613                    version = 42
614                    """),
615            )]
616            make_sdist(dist_path, files)
617        with contexts.save_pkg_resources_state():
618            with contexts.tempdir() as temp_dir:
619                test_pkg = create_setup_requires_package(
620                    temp_dir, setup_attrs=dict(version='attr: foobar.version'),
621                    make_package=make_dependency_sdist,
622                    use_setup_cfg=use_setup_cfg + ('version',),
623                )
624                test_setup_py = os.path.join(test_pkg, 'setup.py')
625                with contexts.quiet() as (stdout, stderr):
626                    run_setup(test_setup_py, [str('--version')])
627                lines = stdout.readlines()
628                assert len(lines) > 0
629                assert lines[-1].strip() == '42'
630
631    def test_setup_requires_honors_pip_env(self, mock_index, monkeypatch):
632        monkeypatch.setenv(str('PIP_RETRIES'), str('0'))
633        monkeypatch.setenv(str('PIP_TIMEOUT'), str('0'))
634        monkeypatch.setenv('PIP_NO_INDEX', 'false')
635        monkeypatch.setenv(str('PIP_INDEX_URL'), mock_index.url)
636        with contexts.save_pkg_resources_state():
637            with contexts.tempdir() as temp_dir:
638                test_pkg = create_setup_requires_package(
639                    temp_dir, 'python-xlib', '0.19',
640                    setup_attrs=dict(dependency_links=[]))
641                test_setup_cfg = os.path.join(test_pkg, 'setup.cfg')
642                with open(test_setup_cfg, 'w') as fp:
643                    fp.write(DALS(
644                        '''
645                        [easy_install]
646                        index_url = https://pypi.org/legacy/
647                        '''))
648                test_setup_py = os.path.join(test_pkg, 'setup.py')
649                with pytest.raises(distutils.errors.DistutilsError):
650                    run_setup(test_setup_py, [str('--version')])
651        assert len(mock_index.requests) == 1
652        assert mock_index.requests[0].path == '/python-xlib/'
653
654    def test_setup_requires_with_pep508_url(self, mock_index, monkeypatch):
655        monkeypatch.setenv(str('PIP_RETRIES'), str('0'))
656        monkeypatch.setenv(str('PIP_TIMEOUT'), str('0'))
657        monkeypatch.setenv(str('PIP_INDEX_URL'), mock_index.url)
658        with contexts.save_pkg_resources_state():
659            with contexts.tempdir() as temp_dir:
660                dep_sdist = os.path.join(temp_dir, 'dep.tar.gz')
661                make_trivial_sdist(dep_sdist, 'dependency', '42')
662                dep_url = path_to_url(dep_sdist, authority='localhost')
663                test_pkg = create_setup_requires_package(
664                    temp_dir,
665                    # Ignored (overridden by setup_attrs)
666                    'python-xlib', '0.19',
667                    setup_attrs=dict(
668                        setup_requires='dependency @ %s' % dep_url))
669                test_setup_py = os.path.join(test_pkg, 'setup.py')
670                run_setup(test_setup_py, [str('--version')])
671        assert len(mock_index.requests) == 0
672
673    def test_setup_requires_with_allow_hosts(self, mock_index):
674        ''' The `allow-hosts` option in not supported anymore. '''
675        files = {
676            'test_pkg': {
677                'setup.py': DALS('''
678                    from setuptools import setup
679                    setup(setup_requires='python-xlib')
680                    '''),
681                'setup.cfg': DALS('''
682                    [easy_install]
683                    allow_hosts = *
684                    '''),
685            }
686        }
687        with contexts.save_pkg_resources_state():
688            with contexts.tempdir() as temp_dir:
689                path.build(files, prefix=temp_dir)
690                setup_py = str(pathlib.Path(temp_dir, 'test_pkg', 'setup.py'))
691                with pytest.raises(distutils.errors.DistutilsError):
692                    run_setup(setup_py, [str('--version')])
693        assert len(mock_index.requests) == 0
694
695    def test_setup_requires_with_python_requires(self, monkeypatch, tmpdir):
696        ''' Check `python_requires` is honored. '''
697        monkeypatch.setenv(str('PIP_RETRIES'), str('0'))
698        monkeypatch.setenv(str('PIP_TIMEOUT'), str('0'))
699        monkeypatch.setenv(str('PIP_NO_INDEX'), str('1'))
700        monkeypatch.setenv(str('PIP_VERBOSE'), str('1'))
701        dep_1_0_sdist = 'dep-1.0.tar.gz'
702        dep_1_0_url = path_to_url(str(tmpdir / dep_1_0_sdist))
703        dep_1_0_python_requires = '>=2.7'
704        make_python_requires_sdist(
705            str(tmpdir / dep_1_0_sdist), 'dep', '1.0', dep_1_0_python_requires)
706        dep_2_0_sdist = 'dep-2.0.tar.gz'
707        dep_2_0_url = path_to_url(str(tmpdir / dep_2_0_sdist))
708        dep_2_0_python_requires = '!=' + '.'.join(
709            map(str, sys.version_info[:2])) + '.*'
710        make_python_requires_sdist(
711            str(tmpdir / dep_2_0_sdist), 'dep', '2.0', dep_2_0_python_requires)
712        index = tmpdir / 'index.html'
713        index.write_text(DALS(
714            '''
715            <!DOCTYPE html>
716            <html><head><title>Links for dep</title></head>
717            <body>
718                <h1>Links for dep</h1>
719                <a href="{dep_1_0_url}" data-requires-python="{dep_1_0_python_requires}">{dep_1_0_sdist}</a><br/>
720                <a href="{dep_2_0_url}" data-requires-python="{dep_2_0_python_requires}">{dep_2_0_sdist}</a><br/>
721            </body>
722            </html>
723            ''').format(  # noqa
724                dep_1_0_url=dep_1_0_url,
725                dep_1_0_sdist=dep_1_0_sdist,
726                dep_1_0_python_requires=dep_1_0_python_requires,
727                dep_2_0_url=dep_2_0_url,
728                dep_2_0_sdist=dep_2_0_sdist,
729                dep_2_0_python_requires=dep_2_0_python_requires,
730        ), 'utf-8')
731        index_url = path_to_url(str(index))
732        with contexts.save_pkg_resources_state():
733            test_pkg = create_setup_requires_package(
734                str(tmpdir),
735                'python-xlib', '0.19',  # Ignored (overridden by setup_attrs).
736                setup_attrs=dict(
737                    setup_requires='dep', dependency_links=[index_url]))
738            test_setup_py = os.path.join(test_pkg, 'setup.py')
739            run_setup(test_setup_py, [str('--version')])
740        eggs = list(map(str, pkg_resources.find_distributions(
741            os.path.join(test_pkg, '.eggs'))))
742        assert eggs == ['dep 1.0']
743
744    @pytest.mark.parametrize(
745        'with_dependency_links_in_setup_py',
746        (False, True))
747    def test_setup_requires_with_find_links_in_setup_cfg(
748            self, monkeypatch,
749            with_dependency_links_in_setup_py):
750        monkeypatch.setenv(str('PIP_RETRIES'), str('0'))
751        monkeypatch.setenv(str('PIP_TIMEOUT'), str('0'))
752        with contexts.save_pkg_resources_state():
753            with contexts.tempdir() as temp_dir:
754                make_trivial_sdist(
755                    os.path.join(temp_dir, 'python-xlib-42.tar.gz'),
756                    'python-xlib',
757                    '42')
758                test_pkg = os.path.join(temp_dir, 'test_pkg')
759                test_setup_py = os.path.join(test_pkg, 'setup.py')
760                test_setup_cfg = os.path.join(test_pkg, 'setup.cfg')
761                os.mkdir(test_pkg)
762                with open(test_setup_py, 'w') as fp:
763                    if with_dependency_links_in_setup_py:
764                        dependency_links = [os.path.join(temp_dir, 'links')]
765                    else:
766                        dependency_links = []
767                    fp.write(DALS(
768                        '''
769                        from setuptools import installer, setup
770                        setup(setup_requires='python-xlib==42',
771                        dependency_links={dependency_links!r})
772                        ''').format(
773                                    dependency_links=dependency_links))
774                with open(test_setup_cfg, 'w') as fp:
775                    fp.write(DALS(
776                        '''
777                        [easy_install]
778                        index_url = {index_url}
779                        find_links = {find_links}
780                        ''').format(index_url=os.path.join(temp_dir, 'index'),
781                                    find_links=temp_dir))
782                run_setup(test_setup_py, [str('--version')])
783
784    def test_setup_requires_with_transitive_extra_dependency(
785            self, monkeypatch):
786        # Use case: installing a package with a build dependency on
787        # an already installed `dep[extra]`, which in turn depends
788        # on `extra_dep` (whose is not already installed).
789        with contexts.save_pkg_resources_state():
790            with contexts.tempdir() as temp_dir:
791                # Create source distribution for `extra_dep`.
792                make_trivial_sdist(
793                    os.path.join(temp_dir, 'extra_dep-1.0.tar.gz'),
794                    'extra_dep', '1.0')
795                # Create source tree for `dep`.
796                dep_pkg = os.path.join(temp_dir, 'dep')
797                os.mkdir(dep_pkg)
798                path.build({
799                    'setup.py':
800                    DALS("""
801                          import setuptools
802                          setuptools.setup(
803                              name='dep', version='2.0',
804                              extras_require={'extra': ['extra_dep']},
805                          )
806                         """),
807                    'setup.cfg': '',
808                }, prefix=dep_pkg)
809                # "Install" dep.
810                run_setup(
811                    os.path.join(dep_pkg, 'setup.py'), [str('dist_info')])
812                working_set.add_entry(dep_pkg)
813                # Create source tree for test package.
814                test_pkg = os.path.join(temp_dir, 'test_pkg')
815                test_setup_py = os.path.join(test_pkg, 'setup.py')
816                os.mkdir(test_pkg)
817                with open(test_setup_py, 'w') as fp:
818                    fp.write(DALS(
819                        '''
820                        from setuptools import installer, setup
821                        setup(setup_requires='dep[extra]')
822                        '''))
823                # Check...
824                monkeypatch.setenv(str('PIP_FIND_LINKS'), str(temp_dir))
825                monkeypatch.setenv(str('PIP_NO_INDEX'), str('1'))
826                monkeypatch.setenv(str('PIP_RETRIES'), str('0'))
827                monkeypatch.setenv(str('PIP_TIMEOUT'), str('0'))
828                run_setup(test_setup_py, [str('--version')])
829
830
831def make_trivial_sdist(dist_path, distname, version):
832    """
833    Create a simple sdist tarball at dist_path, containing just a simple
834    setup.py.
835    """
836
837    make_sdist(dist_path, [
838        ('setup.py',
839         DALS("""\
840             import setuptools
841             setuptools.setup(
842                 name=%r,
843                 version=%r
844             )
845         """ % (distname, version))),
846        ('setup.cfg', ''),
847    ])
848
849
850def make_nspkg_sdist(dist_path, distname, version):
851    """
852    Make an sdist tarball with distname and version which also contains one
853    package with the same name as distname.  The top-level package is
854    designated a namespace package).
855    """
856
857    parts = distname.split('.')
858    nspackage = parts[0]
859
860    packages = ['.'.join(parts[:idx]) for idx in range(1, len(parts) + 1)]
861
862    setup_py = DALS("""\
863        import setuptools
864        setuptools.setup(
865            name=%r,
866            version=%r,
867            packages=%r,
868            namespace_packages=[%r]
869        )
870    """ % (distname, version, packages, nspackage))
871
872    init = "__import__('pkg_resources').declare_namespace(__name__)"
873
874    files = [('setup.py', setup_py),
875             (os.path.join(nspackage, '__init__.py'), init)]
876    for package in packages[1:]:
877        filename = os.path.join(*(package.split('.') + ['__init__.py']))
878        files.append((filename, ''))
879
880    make_sdist(dist_path, files)
881
882
883def make_python_requires_sdist(dist_path, distname, version, python_requires):
884    make_sdist(dist_path, [
885        (
886            'setup.py',
887            DALS("""\
888                import setuptools
889                setuptools.setup(
890                  name={name!r},
891                  version={version!r},
892                  python_requires={python_requires!r},
893                )
894                """).format(
895                name=distname, version=version,
896                python_requires=python_requires)),
897        ('setup.cfg', ''),
898    ])
899
900
901def make_sdist(dist_path, files):
902    """
903    Create a simple sdist tarball at dist_path, containing the files
904    listed in ``files`` as ``(filename, content)`` tuples.
905    """
906
907    # Distributions with only one file don't play well with pip.
908    assert len(files) > 1
909    with tarfile.open(dist_path, 'w:gz') as dist:
910        for filename, content in files:
911            file_bytes = io.BytesIO(content.encode('utf-8'))
912            file_info = tarfile.TarInfo(name=filename)
913            file_info.size = len(file_bytes.getvalue())
914            file_info.mtime = int(time.time())
915            dist.addfile(file_info, fileobj=file_bytes)
916
917
918def create_setup_requires_package(path, distname='foobar', version='0.1',
919                                  make_package=make_trivial_sdist,
920                                  setup_py_template=None, setup_attrs={},
921                                  use_setup_cfg=()):
922    """Creates a source tree under path for a trivial test package that has a
923    single requirement in setup_requires--a tarball for that requirement is
924    also created and added to the dependency_links argument.
925
926    ``distname`` and ``version`` refer to the name/version of the package that
927    the test package requires via ``setup_requires``.  The name of the test
928    package itself is just 'test_pkg'.
929    """
930
931    test_setup_attrs = {
932        'name': 'test_pkg', 'version': '0.0',
933        'setup_requires': ['%s==%s' % (distname, version)],
934        'dependency_links': [os.path.abspath(path)]
935    }
936    test_setup_attrs.update(setup_attrs)
937
938    test_pkg = os.path.join(path, 'test_pkg')
939    os.mkdir(test_pkg)
940
941    # setup.cfg
942    if use_setup_cfg:
943        options = []
944        metadata = []
945        for name in use_setup_cfg:
946            value = test_setup_attrs.pop(name)
947            if name in 'name version'.split():
948                section = metadata
949            else:
950                section = options
951            if isinstance(value, (tuple, list)):
952                value = ';'.join(value)
953            section.append('%s: %s' % (name, value))
954        test_setup_cfg_contents = DALS(
955            """
956            [metadata]
957            {metadata}
958            [options]
959            {options}
960            """
961        ).format(
962            options='\n'.join(options),
963            metadata='\n'.join(metadata),
964        )
965    else:
966        test_setup_cfg_contents = ''
967    with open(os.path.join(test_pkg, 'setup.cfg'), 'w') as f:
968        f.write(test_setup_cfg_contents)
969
970    # setup.py
971    if setup_py_template is None:
972        setup_py_template = DALS("""\
973            import setuptools
974            setuptools.setup(**%r)
975        """)
976    with open(os.path.join(test_pkg, 'setup.py'), 'w') as f:
977        f.write(setup_py_template % test_setup_attrs)
978
979    foobar_path = os.path.join(path, '%s-%s.tar.gz' % (distname, version))
980    make_package(foobar_path, distname, version)
981
982    return test_pkg
983
984
985@pytest.mark.skipif(
986    sys.platform.startswith('java') and ei.is_sh(sys.executable),
987    reason="Test cannot run under java when executable is sh"
988)
989class TestScriptHeader:
990    non_ascii_exe = '/Users/José/bin/python'
991    exe_with_spaces = r'C:\Program Files\Python36\python.exe'
992
993    def test_get_script_header(self):
994        expected = '#!%s\n' % ei.nt_quote_arg(os.path.normpath(sys.executable))
995        actual = ei.ScriptWriter.get_header('#!/usr/local/bin/python')
996        assert actual == expected
997
998    def test_get_script_header_args(self):
999        expected = '#!%s -x\n' % ei.nt_quote_arg(
1000            os.path.normpath(sys.executable))
1001        actual = ei.ScriptWriter.get_header('#!/usr/bin/python -x')
1002        assert actual == expected
1003
1004    def test_get_script_header_non_ascii_exe(self):
1005        actual = ei.ScriptWriter.get_header(
1006            '#!/usr/bin/python',
1007            executable=self.non_ascii_exe)
1008        expected = str('#!%s -x\n') % self.non_ascii_exe
1009        assert actual == expected
1010
1011    def test_get_script_header_exe_with_spaces(self):
1012        actual = ei.ScriptWriter.get_header(
1013            '#!/usr/bin/python',
1014            executable='"' + self.exe_with_spaces + '"')
1015        expected = '#!"%s"\n' % self.exe_with_spaces
1016        assert actual == expected
1017
1018
1019class TestCommandSpec:
1020    def test_custom_launch_command(self):
1021        """
1022        Show how a custom CommandSpec could be used to specify a #! executable
1023        which takes parameters.
1024        """
1025        cmd = ei.CommandSpec(['/usr/bin/env', 'python3'])
1026        assert cmd.as_header() == '#!/usr/bin/env python3\n'
1027
1028    def test_from_param_for_CommandSpec_is_passthrough(self):
1029        """
1030        from_param should return an instance of a CommandSpec
1031        """
1032        cmd = ei.CommandSpec(['python'])
1033        cmd_new = ei.CommandSpec.from_param(cmd)
1034        assert cmd is cmd_new
1035
1036    @mock.patch('sys.executable', TestScriptHeader.exe_with_spaces)
1037    @mock.patch.dict(os.environ)
1038    def test_from_environment_with_spaces_in_executable(self):
1039        os.environ.pop('__PYVENV_LAUNCHER__', None)
1040        cmd = ei.CommandSpec.from_environment()
1041        assert len(cmd) == 1
1042        assert cmd.as_header().startswith('#!"')
1043
1044    def test_from_simple_string_uses_shlex(self):
1045        """
1046        In order to support `executable = /usr/bin/env my-python`, make sure
1047        from_param invokes shlex on that input.
1048        """
1049        cmd = ei.CommandSpec.from_param('/usr/bin/env my-python')
1050        assert len(cmd) == 2
1051        assert '"' not in cmd.as_header()
1052
1053
1054class TestWindowsScriptWriter:
1055    def test_header(self):
1056        hdr = ei.WindowsScriptWriter.get_header('')
1057        assert hdr.startswith('#!')
1058        assert hdr.endswith('\n')
1059        hdr = hdr.lstrip('#!')
1060        hdr = hdr.rstrip('\n')
1061        # header should not start with an escaped quote
1062        assert not hdr.startswith('\\"')
1063
1064
1065VersionStub = namedtuple("VersionStub", "major, minor, micro, releaselevel, serial")
1066
1067
1068def test_use_correct_python_version_string(tmpdir, tmpdir_cwd, monkeypatch):
1069    # In issue #3001, easy_install wrongly uses the `python3.1` directory
1070    # when the interpreter is `python3.10` and the `--user` option is given.
1071    # See pypa/setuptools#3001.
1072    dist = Distribution()
1073    cmd = dist.get_command_obj('easy_install')
1074    cmd.args = ['ok']
1075    cmd.optimize = 0
1076    cmd.user = True
1077    cmd.install_userbase = str(tmpdir)
1078    cmd.install_usersite = None
1079    install_cmd = dist.get_command_obj('install')
1080    install_cmd.install_userbase = str(tmpdir)
1081    install_cmd.install_usersite = None
1082
1083    with monkeypatch.context() as patch, warnings.catch_warnings():
1084        warnings.simplefilter("ignore")
1085        version = '3.10.1 (main, Dec 21 2021, 09:17:12) [GCC 10.2.1 20210110]'
1086        info = VersionStub(3, 10, 1, "final", 0)
1087        patch.setattr('site.ENABLE_USER_SITE', True)
1088        patch.setattr('sys.version', version)
1089        patch.setattr('sys.version_info', info)
1090        patch.setattr(cmd, 'create_home_path', mock.Mock())
1091        cmd.finalize_options()
1092
1093    name = "pypy" if hasattr(sys, 'pypy_version_info') else "python"
1094    install_dir = cmd.install_dir.lower()
1095
1096    # In some platforms (e.g. Windows), install_dir is mostly determined
1097    # via `sysconfig`, which define constants eagerly at module creation.
1098    # This means that monkeypatching `sys.version` to emulate 3.10 for testing
1099    # may have no effect.
1100    # The safest test here is to rely on the fact that 3.1 is no longer
1101    # supported/tested, and make sure that if 'python3.1' ever appears in the string
1102    # it is followed by another digit (e.g. 'python3.10').
1103    if re.search(name + r'3\.?1', install_dir):
1104        assert re.search(name + r'3\.?1\d', install_dir)
1105
1106    # The following "variables" are used for interpolation in distutils
1107    # installation schemes, so it should be fair to treat them as "semi-public",
1108    # or at least public enough so we can have a test to make sure they are correct
1109    assert cmd.config_vars['py_version'] == '3.10.1'
1110    assert cmd.config_vars['py_version_short'] == '3.10'
1111    assert cmd.config_vars['py_version_nodot'] == '310'
1112