1import json
2import os
3import pathlib
4import sys
5import tempfile
6from unittest import TestCase, SkipTest
7from unittest.mock import patch
8
9import pytest
10from testpath import (
11    assert_isfile, assert_isdir, assert_islink, assert_not_path_exists, MockCommand
12)
13
14from flit import install
15from flit.install import Installer, _requires_dist_to_pip_requirement, DependencyError
16import flit_core.tests
17
18samples_dir = pathlib.Path(__file__).parent / 'samples'
19core_samples_dir = pathlib.Path(flit_core.tests.__file__).parent / 'samples'
20
21class InstallTests(TestCase):
22    def setUp(self):
23        td = tempfile.TemporaryDirectory()
24        scripts_dir = os.path.join(td.name, 'scripts')
25        purelib_dir = os.path.join(td.name, 'site-packages')
26        self.addCleanup(td.cleanup)
27        self.get_dirs_patch = patch('flit.install.get_dirs',
28                return_value={'scripts': scripts_dir, 'purelib': purelib_dir})
29        self.get_dirs_patch.start()
30        self.tmpdir = pathlib.Path(td.name)
31
32    def tearDown(self):
33        self.get_dirs_patch.stop()
34
35    def _assert_direct_url(self, directory, package, version, expected_editable):
36        direct_url_file = (
37            self.tmpdir
38            / 'site-packages'
39            / '{}-{}.dist-info'.format(package, version)
40            / 'direct_url.json'
41        )
42        assert_isfile(direct_url_file)
43        with direct_url_file.open() as f:
44            direct_url = json.load(f)
45            assert direct_url['url'].startswith('file:///')
46            assert direct_url['url'] == directory.as_uri()
47            assert direct_url['dir_info'].get('editable') is expected_editable
48
49    def test_install_module(self):
50        Installer.from_ini_path(samples_dir / 'module1_toml' / 'pyproject.toml').install_directly()
51        assert_isfile(self.tmpdir / 'site-packages' / 'module1.py')
52        assert_isdir(self.tmpdir / 'site-packages' / 'module1-0.1.dist-info')
53        self._assert_direct_url(
54            samples_dir / 'module1_toml', 'module1', '0.1', expected_editable=False
55        )
56
57    def test_install_module_pep621(self):
58        Installer.from_ini_path(
59            core_samples_dir / 'pep621_nodynamic' / 'pyproject.toml',
60        ).install_directly()
61        assert_isfile(self.tmpdir / 'site-packages' / 'module1.py')
62        assert_isdir(self.tmpdir / 'site-packages' / 'module1-0.3.dist-info')
63        self._assert_direct_url(
64            core_samples_dir / 'pep621_nodynamic', 'module1', '0.3',
65            expected_editable=False
66        )
67
68    def test_install_package(self):
69        oldcwd = os.getcwd()
70        os.chdir(str(samples_dir / 'package1'))
71        try:
72            Installer.from_ini_path(pathlib.Path('pyproject.toml')).install_directly()
73        finally:
74            os.chdir(oldcwd)
75        assert_isdir(self.tmpdir / 'site-packages' / 'package1')
76        assert_isdir(self.tmpdir / 'site-packages' / 'package1-0.1.dist-info')
77        assert_isfile(self.tmpdir / 'scripts' / 'pkg_script')
78        with (self.tmpdir / 'scripts' / 'pkg_script').open() as f:
79            assert f.readline().strip() == "#!" + sys.executable
80        self._assert_direct_url(
81            samples_dir / 'package1', 'package1', '0.1', expected_editable=False
82        )
83
84    def test_install_module_in_src(self):
85        oldcwd = os.getcwd()
86        os.chdir(samples_dir / 'packageinsrc')
87        try:
88            Installer.from_ini_path(pathlib.Path('pyproject.toml')).install_directly()
89        finally:
90            os.chdir(oldcwd)
91        assert_isfile(self.tmpdir / 'site-packages' / 'module1.py')
92        assert_isdir(self.tmpdir / 'site-packages' / 'module1-0.1.dist-info')
93
94    def test_install_ns_package_native(self):
95        Installer.from_ini_path(samples_dir / 'ns1-pkg' / 'pyproject.toml').install_directly()
96        assert_isdir(self.tmpdir / 'site-packages' / 'ns1')
97        assert_isfile(self.tmpdir / 'site-packages' / 'ns1' / 'pkg' / '__init__.py')
98        assert_not_path_exists(self.tmpdir / 'site-packages' / 'ns1' / '__init__.py')
99        assert_isdir(self.tmpdir / 'site-packages' / 'ns1_pkg-0.1.dist-info')
100
101    def test_install_ns_package_module_native(self):
102        Installer.from_ini_path(samples_dir / 'ns1-pkg-mod' / 'pyproject.toml').install_directly()
103        assert_isfile(self.tmpdir / 'site-packages' / 'ns1' / 'module.py')
104        assert_not_path_exists(self.tmpdir / 'site-packages' / 'ns1' / '__init__.py')
105
106    def test_install_ns_package_native_symlink(self):
107        if os.name == 'nt':
108            raise SkipTest('symlink')
109        Installer.from_ini_path(
110            samples_dir / 'ns1-pkg' / 'pyproject.toml', symlink=True
111        ).install_directly()
112        Installer.from_ini_path(
113            samples_dir / 'ns1-pkg2' / 'pyproject.toml', symlink=True
114        ).install_directly()
115        Installer.from_ini_path(
116            samples_dir / 'ns1-pkg-mod' / 'pyproject.toml', symlink=True
117        ).install_directly()
118        assert_isdir(self.tmpdir / 'site-packages' / 'ns1')
119        assert_isdir(self.tmpdir / 'site-packages' / 'ns1' / 'pkg')
120        assert_islink(self.tmpdir / 'site-packages' / 'ns1' / 'pkg',
121                      to=str(samples_dir / 'ns1-pkg' / 'ns1' / 'pkg'))
122        assert_isdir(self.tmpdir / 'site-packages' / 'ns1_pkg-0.1.dist-info')
123
124        assert_isdir(self.tmpdir / 'site-packages' / 'ns1' / 'pkg2')
125        assert_islink(self.tmpdir / 'site-packages' / 'ns1' / 'pkg2',
126                      to=str(samples_dir / 'ns1-pkg2' / 'ns1' / 'pkg2'))
127        assert_isdir(self.tmpdir / 'site-packages' / 'ns1_pkg2-0.1.dist-info')
128
129        assert_islink(self.tmpdir / 'site-packages' / 'ns1' / 'module.py',
130                      to=samples_dir / 'ns1-pkg-mod' / 'ns1' / 'module.py')
131        assert_isdir(self.tmpdir / 'site-packages' / 'ns1_module-0.1.dist-info')
132
133    def test_install_ns_package_pth_file(self):
134        Installer.from_ini_path(
135            samples_dir / 'ns1-pkg' / 'pyproject.toml', pth=True
136        ).install_directly()
137
138        pth_file = self.tmpdir / 'site-packages' / 'ns1.pkg.pth'
139        assert_isfile(pth_file)
140        assert pth_file.read_text('utf-8').strip() == str(samples_dir / 'ns1-pkg')
141
142    def test_symlink_package(self):
143        if os.name == 'nt':
144            raise SkipTest("symlink")
145        Installer.from_ini_path(samples_dir / 'package1' / 'pyproject.toml', symlink=True).install()
146        assert_islink(self.tmpdir / 'site-packages' / 'package1',
147                      to=samples_dir / 'package1' / 'package1')
148        assert_isfile(self.tmpdir / 'scripts' / 'pkg_script')
149        with (self.tmpdir / 'scripts' / 'pkg_script').open() as f:
150            assert f.readline().strip() == "#!" + sys.executable
151        self._assert_direct_url(
152            samples_dir / 'package1', 'package1', '0.1', expected_editable=True
153        )
154
155    def test_symlink_module_pep621(self):
156        if os.name == 'nt':
157            raise SkipTest("symlink")
158        Installer.from_ini_path(
159            core_samples_dir / 'pep621_nodynamic' / 'pyproject.toml', symlink=True
160        ).install_directly()
161        assert_islink(self.tmpdir / 'site-packages' / 'module1.py',
162                      to=core_samples_dir / 'pep621_nodynamic' / 'module1.py')
163        assert_isdir(self.tmpdir / 'site-packages' / 'module1-0.3.dist-info')
164        self._assert_direct_url(
165            core_samples_dir / 'pep621_nodynamic', 'module1', '0.3',
166            expected_editable=True
167        )
168
169    def test_symlink_module_in_src(self):
170        oldcwd = os.getcwd()
171        os.chdir(samples_dir / 'packageinsrc')
172        try:
173            Installer.from_ini_path(
174                pathlib.Path('pyproject.toml'), symlink=True
175            ).install_directly()
176        finally:
177            os.chdir(oldcwd)
178        assert_islink(self.tmpdir / 'site-packages' / 'module1.py',
179                      to=(samples_dir / 'packageinsrc' / 'src' / 'module1.py'))
180        assert_isdir(self.tmpdir / 'site-packages' / 'module1-0.1.dist-info')
181
182    def test_pth_package(self):
183        Installer.from_ini_path(samples_dir / 'package1' / 'pyproject.toml', pth=True).install()
184        assert_isfile(self.tmpdir / 'site-packages' / 'package1.pth')
185        with open(str(self.tmpdir / 'site-packages' / 'package1.pth')) as f:
186            assert f.read() == str(samples_dir / 'package1')
187        assert_isfile(self.tmpdir / 'scripts' / 'pkg_script')
188        self._assert_direct_url(
189            samples_dir / 'package1', 'package1', '0.1', expected_editable=True
190        )
191
192    def test_pth_module_in_src(self):
193        oldcwd = os.getcwd()
194        os.chdir(samples_dir / 'packageinsrc')
195        try:
196            Installer.from_ini_path(
197                pathlib.Path('pyproject.toml'), pth=True
198            ).install_directly()
199        finally:
200            os.chdir(oldcwd)
201        pth_path = self.tmpdir / 'site-packages' / 'module1.pth'
202        assert_isfile(pth_path)
203        assert pth_path.read_text('utf-8').strip() == str(
204            samples_dir / 'packageinsrc' / 'src'
205        )
206        assert_isdir(self.tmpdir / 'site-packages' / 'module1-0.1.dist-info')
207
208    def test_dist_name(self):
209        Installer.from_ini_path(samples_dir / 'altdistname' / 'pyproject.toml').install_directly()
210        assert_isdir(self.tmpdir / 'site-packages' / 'package1')
211        assert_isdir(self.tmpdir / 'site-packages' / 'package_dist1-0.1.dist-info')
212
213    def test_entry_points(self):
214        Installer.from_ini_path(samples_dir / 'entrypoints_valid' / 'pyproject.toml').install_directly()
215        assert_isfile(self.tmpdir / 'site-packages' / 'package1-0.1.dist-info' / 'entry_points.txt')
216
217    def test_pip_install(self):
218        ins = Installer.from_ini_path(samples_dir / 'package1' / 'pyproject.toml', python='mock_python',
219                        user=False)
220
221        with MockCommand('mock_python') as mock_py:
222            ins.install()
223
224        calls = mock_py.get_calls()
225        assert len(calls) == 1
226        cmd = calls[0]['argv']
227        assert cmd[1:4] == ['-m', 'pip', 'install']
228        assert cmd[4].endswith('package1')
229
230    def test_symlink_other_python(self):
231        if os.name == 'nt':
232            raise SkipTest('symlink')
233        (self.tmpdir / 'site-packages2').mkdir()
234        (self.tmpdir / 'scripts2').mkdir()
235
236        # Called by Installer._auto_user() :
237        script1 = ("#!{python}\n"
238                   "import sysconfig\n"
239                   "print(True)\n"   # site.ENABLE_USER_SITE
240                   "print({purelib!r})"  # sysconfig.get_path('purelib')
241                  ).format(python=sys.executable,
242                           purelib=str(self.tmpdir / 'site-packages2'))
243
244        # Called by Installer._get_dirs() :
245        script2 = ("#!{python}\n"
246                   "import json, sys\n"
247                   "json.dump({{'purelib': {purelib!r}, 'scripts': {scripts!r} }}, "
248                   "sys.stdout)"
249                  ).format(python=sys.executable,
250                           purelib=str(self.tmpdir / 'site-packages2'),
251                           scripts=str(self.tmpdir / 'scripts2'))
252
253        with MockCommand('mock_python', content=script1):
254            ins = Installer.from_ini_path(samples_dir / 'package1' / 'pyproject.toml', python='mock_python',
255                      symlink=True)
256        with MockCommand('mock_python', content=script2):
257            ins.install()
258
259        assert_islink(self.tmpdir / 'site-packages2' / 'package1',
260                      to=samples_dir / 'package1' / 'package1')
261        assert_isfile(self.tmpdir / 'scripts2' / 'pkg_script')
262        with (self.tmpdir / 'scripts2' / 'pkg_script').open() as f:
263            assert f.readline().strip() == "#!mock_python"
264
265    def test_install_requires(self):
266        ins = Installer.from_ini_path(samples_dir / 'requires-requests.toml',
267                        user=False, python='mock_python')
268
269        with MockCommand('mock_python') as mockpy:
270            ins.install_requirements()
271        calls = mockpy.get_calls()
272        assert len(calls) == 1
273        assert calls[0]['argv'][1:5] == ['-m', 'pip', 'install', '-r']
274
275    def test_install_reqs_my_python_if_needed_pep621(self):
276        ins = Installer.from_ini_path(
277            core_samples_dir / 'pep621_nodynamic' / 'pyproject.toml',
278            deps='none',
279        )
280
281        # This shouldn't try to get version & docstring from the module
282        ins.install_reqs_my_python_if_needed()
283
284    def test_extras_error(self):
285        with pytest.raises(DependencyError):
286            Installer.from_ini_path(samples_dir / 'requires-requests.toml',
287                            user=False, deps='none', extras='dev')
288
289@pytest.mark.parametrize(('deps', 'extras', 'installed'), [
290    ('none', [], set()),
291    ('develop', [], {'pytest ;', 'toml ;'}),
292    ('production', [], {'toml ;'}),
293    ('all', [], {'toml ;', 'pytest ;', 'requests ;'}),
294])
295def test_install_requires_extra(deps, extras, installed):
296    it = InstallTests()
297    try:
298        it.setUp()
299        ins = Installer.from_ini_path(samples_dir / 'extras' / 'pyproject.toml', python='mock_python',
300                        user=False, deps=deps, extras=extras)
301
302        cmd = MockCommand('mock_python')
303        get_reqs = (
304            "#!{python}\n"
305            "import sys\n"
306            "with open({recording_file!r}, 'wb') as w, open(sys.argv[-1], 'rb') as r:\n"
307            "    w.write(r.read())"
308        ).format(python=sys.executable, recording_file=cmd.recording_file)
309        cmd.content = get_reqs
310
311        with cmd as mock_py:
312            ins.install_requirements()
313        with open(mock_py.recording_file) as f:
314            str_deps = f.read()
315        deps = str_deps.split('\n') if str_deps else []
316
317        assert set(deps) == installed
318    finally:
319        it.tearDown()
320
321def test_requires_dist_to_pip_requirement():
322    rd = 'pathlib2 (>=2.3); python_version == "2.7"'
323    assert _requires_dist_to_pip_requirement(rd) == \
324        'pathlib2>=2.3 ; python_version == "2.7"'
325
326def test_test_writable_dir_win():
327    with tempfile.TemporaryDirectory() as td:
328        assert install._test_writable_dir_win(td) is True
329
330        # Ironically, I don't know how to make a non-writable dir on Windows,
331        # so although the functionality is for Windows, the test is for Posix
332        if os.name != 'posix':
333            return
334
335        # Remove write permissions from the directory
336        os.chmod(td, 0o444)
337        try:
338            assert install._test_writable_dir_win(td) is False
339        finally:
340            os.chmod(td, 0o644)
341