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