1# -*- coding: utf-8 -*- 2"""sdist tests""" 3 4from __future__ import print_function, unicode_literals 5 6import os 7import sys 8import tempfile 9import unicodedata 10import contextlib 11import io 12 13from setuptools.extern import six 14from setuptools.extern.six.moves import map 15 16import pytest 17 18import pkg_resources 19from setuptools.command.sdist import sdist 20from setuptools.command.egg_info import manifest_maker 21from setuptools.dist import Distribution 22from setuptools.tests import fail_on_ascii 23from .text import Filenames 24from . import py3_only 25 26 27SETUP_ATTRS = { 28 'name': 'sdist_test', 29 'version': '0.0', 30 'packages': ['sdist_test'], 31 'package_data': {'sdist_test': ['*.txt']}, 32 'data_files': [("data", [os.path.join("d", "e.dat")])], 33} 34 35SETUP_PY = """\ 36from setuptools import setup 37 38setup(**%r) 39""" % SETUP_ATTRS 40 41 42@contextlib.contextmanager 43def quiet(): 44 old_stdout, old_stderr = sys.stdout, sys.stderr 45 sys.stdout, sys.stderr = six.StringIO(), six.StringIO() 46 try: 47 yield 48 finally: 49 sys.stdout, sys.stderr = old_stdout, old_stderr 50 51 52# Convert to POSIX path 53def posix(path): 54 if not six.PY2 and not isinstance(path, str): 55 return path.replace(os.sep.encode('ascii'), b'/') 56 else: 57 return path.replace(os.sep, '/') 58 59 60# HFS Plus uses decomposed UTF-8 61def decompose(path): 62 if isinstance(path, six.text_type): 63 return unicodedata.normalize('NFD', path) 64 try: 65 path = path.decode('utf-8') 66 path = unicodedata.normalize('NFD', path) 67 path = path.encode('utf-8') 68 except UnicodeError: 69 pass # Not UTF-8 70 return path 71 72 73def read_all_bytes(filename): 74 with io.open(filename, 'rb') as fp: 75 return fp.read() 76 77 78def latin1_fail(): 79 try: 80 desc, filename = tempfile.mkstemp(suffix=Filenames.latin_1) 81 os.close(desc) 82 os.remove(filename) 83 except Exception: 84 return True 85 86 87fail_on_latin1_encoded_filenames = pytest.mark.xfail( 88 latin1_fail(), 89 reason="System does not support latin-1 filenames", 90) 91 92 93def touch(path): 94 path.write_text('', encoding='utf-8') 95 96 97class TestSdistTest: 98 @pytest.fixture(autouse=True) 99 def source_dir(self, tmpdir): 100 (tmpdir / 'setup.py').write_text(SETUP_PY, encoding='utf-8') 101 102 # Set up the rest of the test package 103 test_pkg = tmpdir / 'sdist_test' 104 test_pkg.mkdir() 105 data_folder = tmpdir / 'd' 106 data_folder.mkdir() 107 # *.rst was not included in package_data, so c.rst should not be 108 # automatically added to the manifest when not under version control 109 for fname in ['__init__.py', 'a.txt', 'b.txt', 'c.rst']: 110 touch(test_pkg / fname) 111 touch(data_folder / 'e.dat') 112 113 with tmpdir.as_cwd(): 114 yield 115 116 def test_package_data_in_sdist(self): 117 """Regression test for pull request #4: ensures that files listed in 118 package_data are included in the manifest even if they're not added to 119 version control. 120 """ 121 122 dist = Distribution(SETUP_ATTRS) 123 dist.script_name = 'setup.py' 124 cmd = sdist(dist) 125 cmd.ensure_finalized() 126 127 with quiet(): 128 cmd.run() 129 130 manifest = cmd.filelist.files 131 assert os.path.join('sdist_test', 'a.txt') in manifest 132 assert os.path.join('sdist_test', 'b.txt') in manifest 133 assert os.path.join('sdist_test', 'c.rst') not in manifest 134 assert os.path.join('d', 'e.dat') in manifest 135 136 def test_setup_py_exists(self): 137 dist = Distribution(SETUP_ATTRS) 138 dist.script_name = 'foo.py' 139 cmd = sdist(dist) 140 cmd.ensure_finalized() 141 142 with quiet(): 143 cmd.run() 144 145 manifest = cmd.filelist.files 146 assert 'setup.py' in manifest 147 148 def test_setup_py_missing(self): 149 dist = Distribution(SETUP_ATTRS) 150 dist.script_name = 'foo.py' 151 cmd = sdist(dist) 152 cmd.ensure_finalized() 153 154 if os.path.exists("setup.py"): 155 os.remove("setup.py") 156 with quiet(): 157 cmd.run() 158 159 manifest = cmd.filelist.files 160 assert 'setup.py' not in manifest 161 162 def test_setup_py_excluded(self): 163 with open("MANIFEST.in", "w") as manifest_file: 164 manifest_file.write("exclude setup.py") 165 166 dist = Distribution(SETUP_ATTRS) 167 dist.script_name = 'foo.py' 168 cmd = sdist(dist) 169 cmd.ensure_finalized() 170 171 with quiet(): 172 cmd.run() 173 174 manifest = cmd.filelist.files 175 assert 'setup.py' not in manifest 176 177 def test_defaults_case_sensitivity(self, tmpdir): 178 """ 179 Make sure default files (README.*, etc.) are added in a case-sensitive 180 way to avoid problems with packages built on Windows. 181 """ 182 183 touch(tmpdir / 'readme.rst') 184 touch(tmpdir / 'SETUP.cfg') 185 186 dist = Distribution(SETUP_ATTRS) 187 # the extension deliberately capitalized for this test 188 # to make sure the actual filename (not capitalized) gets added 189 # to the manifest 190 dist.script_name = 'setup.PY' 191 cmd = sdist(dist) 192 cmd.ensure_finalized() 193 194 with quiet(): 195 cmd.run() 196 197 # lowercase all names so we can test in a 198 # case-insensitive way to make sure the files 199 # are not included. 200 manifest = map(lambda x: x.lower(), cmd.filelist.files) 201 assert 'readme.rst' not in manifest, manifest 202 assert 'setup.py' not in manifest, manifest 203 assert 'setup.cfg' not in manifest, manifest 204 205 @fail_on_ascii 206 def test_manifest_is_written_with_utf8_encoding(self): 207 # Test for #303. 208 dist = Distribution(SETUP_ATTRS) 209 dist.script_name = 'setup.py' 210 mm = manifest_maker(dist) 211 mm.manifest = os.path.join('sdist_test.egg-info', 'SOURCES.txt') 212 os.mkdir('sdist_test.egg-info') 213 214 # UTF-8 filename 215 filename = os.path.join('sdist_test', 'smörbröd.py') 216 217 # Must create the file or it will get stripped. 218 open(filename, 'w').close() 219 220 # Add UTF-8 filename and write manifest 221 with quiet(): 222 mm.run() 223 mm.filelist.append(filename) 224 mm.write_manifest() 225 226 contents = read_all_bytes(mm.manifest) 227 228 # The manifest should be UTF-8 encoded 229 u_contents = contents.decode('UTF-8') 230 231 # The manifest should contain the UTF-8 filename 232 assert posix(filename) in u_contents 233 234 @py3_only 235 @fail_on_ascii 236 def test_write_manifest_allows_utf8_filenames(self): 237 # Test for #303. 238 dist = Distribution(SETUP_ATTRS) 239 dist.script_name = 'setup.py' 240 mm = manifest_maker(dist) 241 mm.manifest = os.path.join('sdist_test.egg-info', 'SOURCES.txt') 242 os.mkdir('sdist_test.egg-info') 243 244 filename = os.path.join(b'sdist_test', Filenames.utf_8) 245 246 # Must touch the file or risk removal 247 open(filename, "w").close() 248 249 # Add filename and write manifest 250 with quiet(): 251 mm.run() 252 u_filename = filename.decode('utf-8') 253 mm.filelist.files.append(u_filename) 254 # Re-write manifest 255 mm.write_manifest() 256 257 contents = read_all_bytes(mm.manifest) 258 259 # The manifest should be UTF-8 encoded 260 contents.decode('UTF-8') 261 262 # The manifest should contain the UTF-8 filename 263 assert posix(filename) in contents 264 265 # The filelist should have been updated as well 266 assert u_filename in mm.filelist.files 267 268 @py3_only 269 def test_write_manifest_skips_non_utf8_filenames(self): 270 """ 271 Files that cannot be encoded to UTF-8 (specifically, those that 272 weren't originally successfully decoded and have surrogate 273 escapes) should be omitted from the manifest. 274 See https://bitbucket.org/tarek/distribute/issue/303 for history. 275 """ 276 dist = Distribution(SETUP_ATTRS) 277 dist.script_name = 'setup.py' 278 mm = manifest_maker(dist) 279 mm.manifest = os.path.join('sdist_test.egg-info', 'SOURCES.txt') 280 os.mkdir('sdist_test.egg-info') 281 282 # Latin-1 filename 283 filename = os.path.join(b'sdist_test', Filenames.latin_1) 284 285 # Add filename with surrogates and write manifest 286 with quiet(): 287 mm.run() 288 u_filename = filename.decode('utf-8', 'surrogateescape') 289 mm.filelist.append(u_filename) 290 # Re-write manifest 291 mm.write_manifest() 292 293 contents = read_all_bytes(mm.manifest) 294 295 # The manifest should be UTF-8 encoded 296 contents.decode('UTF-8') 297 298 # The Latin-1 filename should have been skipped 299 assert posix(filename) not in contents 300 301 # The filelist should have been updated as well 302 assert u_filename not in mm.filelist.files 303 304 @fail_on_ascii 305 def test_manifest_is_read_with_utf8_encoding(self): 306 # Test for #303. 307 dist = Distribution(SETUP_ATTRS) 308 dist.script_name = 'setup.py' 309 cmd = sdist(dist) 310 cmd.ensure_finalized() 311 312 # Create manifest 313 with quiet(): 314 cmd.run() 315 316 # Add UTF-8 filename to manifest 317 filename = os.path.join(b'sdist_test', Filenames.utf_8) 318 cmd.manifest = os.path.join('sdist_test.egg-info', 'SOURCES.txt') 319 manifest = open(cmd.manifest, 'ab') 320 manifest.write(b'\n' + filename) 321 manifest.close() 322 323 # The file must exist to be included in the filelist 324 open(filename, 'w').close() 325 326 # Re-read manifest 327 cmd.filelist.files = [] 328 with quiet(): 329 cmd.read_manifest() 330 331 # The filelist should contain the UTF-8 filename 332 if not six.PY2: 333 filename = filename.decode('utf-8') 334 assert filename in cmd.filelist.files 335 336 @py3_only 337 @fail_on_latin1_encoded_filenames 338 def test_read_manifest_skips_non_utf8_filenames(self): 339 # Test for #303. 340 dist = Distribution(SETUP_ATTRS) 341 dist.script_name = 'setup.py' 342 cmd = sdist(dist) 343 cmd.ensure_finalized() 344 345 # Create manifest 346 with quiet(): 347 cmd.run() 348 349 # Add Latin-1 filename to manifest 350 filename = os.path.join(b'sdist_test', Filenames.latin_1) 351 cmd.manifest = os.path.join('sdist_test.egg-info', 'SOURCES.txt') 352 manifest = open(cmd.manifest, 'ab') 353 manifest.write(b'\n' + filename) 354 manifest.close() 355 356 # The file must exist to be included in the filelist 357 open(filename, 'w').close() 358 359 # Re-read manifest 360 cmd.filelist.files = [] 361 with quiet(): 362 cmd.read_manifest() 363 364 # The Latin-1 filename should have been skipped 365 filename = filename.decode('latin-1') 366 assert filename not in cmd.filelist.files 367 368 @fail_on_ascii 369 @fail_on_latin1_encoded_filenames 370 def test_sdist_with_utf8_encoded_filename(self): 371 # Test for #303. 372 dist = Distribution(self.make_strings(SETUP_ATTRS)) 373 dist.script_name = 'setup.py' 374 cmd = sdist(dist) 375 cmd.ensure_finalized() 376 377 filename = os.path.join(b'sdist_test', Filenames.utf_8) 378 open(filename, 'w').close() 379 380 with quiet(): 381 cmd.run() 382 383 if sys.platform == 'darwin': 384 filename = decompose(filename) 385 386 if not six.PY2: 387 fs_enc = sys.getfilesystemencoding() 388 389 if sys.platform == 'win32': 390 if fs_enc == 'cp1252': 391 # Python 3 mangles the UTF-8 filename 392 filename = filename.decode('cp1252') 393 assert filename in cmd.filelist.files 394 else: 395 filename = filename.decode('mbcs') 396 assert filename in cmd.filelist.files 397 else: 398 filename = filename.decode('utf-8') 399 assert filename in cmd.filelist.files 400 else: 401 assert filename in cmd.filelist.files 402 403 @classmethod 404 def make_strings(cls, item): 405 if isinstance(item, dict): 406 return { 407 key: cls.make_strings(value) for key, value in item.items()} 408 if isinstance(item, list): 409 return list(map(cls.make_strings, item)) 410 return str(item) 411 412 @fail_on_latin1_encoded_filenames 413 def test_sdist_with_latin1_encoded_filename(self): 414 # Test for #303. 415 dist = Distribution(self.make_strings(SETUP_ATTRS)) 416 dist.script_name = 'setup.py' 417 cmd = sdist(dist) 418 cmd.ensure_finalized() 419 420 # Latin-1 filename 421 filename = os.path.join(b'sdist_test', Filenames.latin_1) 422 open(filename, 'w').close() 423 assert os.path.isfile(filename) 424 425 with quiet(): 426 cmd.run() 427 428 if six.PY2: 429 # Under Python 2 there seems to be no decoded string in the 430 # filelist. However, due to decode and encoding of the 431 # file name to get utf-8 Manifest the latin1 maybe excluded 432 try: 433 # fs_enc should match how one is expect the decoding to 434 # be proformed for the manifest output. 435 fs_enc = sys.getfilesystemencoding() 436 filename.decode(fs_enc) 437 assert filename in cmd.filelist.files 438 except UnicodeDecodeError: 439 filename not in cmd.filelist.files 440 else: 441 # not all windows systems have a default FS encoding of cp1252 442 if sys.platform == 'win32': 443 # Latin-1 is similar to Windows-1252 however 444 # on mbcs filesys it is not in latin-1 encoding 445 fs_enc = sys.getfilesystemencoding() 446 if fs_enc != 'mbcs': 447 fs_enc = 'latin-1' 448 filename = filename.decode(fs_enc) 449 450 assert filename in cmd.filelist.files 451 else: 452 # The Latin-1 filename should have been skipped 453 filename = filename.decode('latin-1') 454 filename not in cmd.filelist.files 455 456 def test_pyproject_toml_in_sdist(self, tmpdir): 457 """ 458 Check if pyproject.toml is included in source distribution if present 459 """ 460 touch(tmpdir / 'pyproject.toml') 461 dist = Distribution(SETUP_ATTRS) 462 dist.script_name = 'setup.py' 463 cmd = sdist(dist) 464 cmd.ensure_finalized() 465 with quiet(): 466 cmd.run() 467 manifest = cmd.filelist.files 468 assert 'pyproject.toml' in manifest 469 470 def test_pyproject_toml_excluded(self, tmpdir): 471 """ 472 Check that pyproject.toml can excluded even if present 473 """ 474 touch(tmpdir / 'pyproject.toml') 475 with open('MANIFEST.in', 'w') as mts: 476 print('exclude pyproject.toml', file=mts) 477 dist = Distribution(SETUP_ATTRS) 478 dist.script_name = 'setup.py' 479 cmd = sdist(dist) 480 cmd.ensure_finalized() 481 with quiet(): 482 cmd.run() 483 manifest = cmd.filelist.files 484 assert 'pyproject.toml' not in manifest 485 486 487def test_default_revctrl(): 488 """ 489 When _default_revctrl was removed from the `setuptools.command.sdist` 490 module in 10.0, it broke some systems which keep an old install of 491 setuptools (Distribute) around. Those old versions require that the 492 setuptools package continue to implement that interface, so this 493 function provides that interface, stubbed. See #320 for details. 494 495 This interface must be maintained until Ubuntu 12.04 is no longer 496 supported (by Setuptools). 497 """ 498 ep_def = 'svn_cvs = setuptools.command.sdist:_default_revctrl' 499 ep = pkg_resources.EntryPoint.parse(ep_def) 500 res = ep.resolve() 501 assert hasattr(res, '__iter__') 502