1# -*- coding: utf-8 -*- 2"""sdist tests""" 3 4import contextlib 5import os 6import shutil 7import sys 8import tempfile 9import itertools 10import io 11from distutils import log 12from distutils.errors import DistutilsTemplateError 13 14from setuptools.command.egg_info import FileList, egg_info, translate_pattern 15from setuptools.dist import Distribution 16from setuptools.tests.textwrap import DALS 17 18import pytest 19 20 21def make_local_path(s): 22 """Converts '/' in a string to os.sep""" 23 return s.replace('/', os.sep) 24 25 26SETUP_ATTRS = { 27 'name': 'app', 28 'version': '0.0', 29 'packages': ['app'], 30} 31 32SETUP_PY = """\ 33from setuptools import setup 34 35setup(**%r) 36""" % SETUP_ATTRS 37 38 39@contextlib.contextmanager 40def quiet(): 41 old_stdout, old_stderr = sys.stdout, sys.stderr 42 sys.stdout, sys.stderr = io.StringIO(), io.StringIO() 43 try: 44 yield 45 finally: 46 sys.stdout, sys.stderr = old_stdout, old_stderr 47 48 49def touch(filename): 50 open(filename, 'w').close() 51 52 53# The set of files always in the manifest, including all files in the 54# .egg-info directory 55default_files = frozenset(map(make_local_path, [ 56 'README.rst', 57 'MANIFEST.in', 58 'setup.py', 59 'app.egg-info/PKG-INFO', 60 'app.egg-info/SOURCES.txt', 61 'app.egg-info/dependency_links.txt', 62 'app.egg-info/top_level.txt', 63 'app/__init__.py', 64])) 65 66 67translate_specs = [ 68 ('foo', ['foo'], ['bar', 'foobar']), 69 ('foo/bar', ['foo/bar'], ['foo/bar/baz', './foo/bar', 'foo']), 70 71 # Glob matching 72 ('*.txt', ['foo.txt', 'bar.txt'], ['foo/foo.txt']), 73 ( 74 'dir/*.txt', 75 ['dir/foo.txt', 'dir/bar.txt', 'dir/.txt'], ['notdir/foo.txt']), 76 ('*/*.py', ['bin/start.py'], []), 77 ('docs/page-?.txt', ['docs/page-9.txt'], ['docs/page-10.txt']), 78 79 # Globstars change what they mean depending upon where they are 80 ( 81 'foo/**/bar', 82 ['foo/bing/bar', 'foo/bing/bang/bar', 'foo/bar'], 83 ['foo/abar'], 84 ), 85 ( 86 'foo/**', 87 ['foo/bar/bing.py', 'foo/x'], 88 ['/foo/x'], 89 ), 90 ( 91 '**', 92 ['x', 'abc/xyz', '@nything'], 93 [], 94 ), 95 96 # Character classes 97 ( 98 'pre[one]post', 99 ['preopost', 'prenpost', 'preepost'], 100 ['prepost', 'preonepost'], 101 ), 102 103 ( 104 'hello[!one]world', 105 ['helloxworld', 'helloyworld'], 106 ['hellooworld', 'helloworld', 'hellooneworld'], 107 ), 108 109 ( 110 '[]one].txt', 111 ['o.txt', '].txt', 'e.txt'], 112 ['one].txt'], 113 ), 114 115 ( 116 'foo[!]one]bar', 117 ['fooybar'], 118 ['foo]bar', 'fooobar', 'fooebar'], 119 ), 120 121] 122""" 123A spec of inputs for 'translate_pattern' and matches and mismatches 124for that input. 125""" 126 127match_params = itertools.chain.from_iterable( 128 zip(itertools.repeat(pattern), matches) 129 for pattern, matches, mismatches in translate_specs 130) 131 132 133@pytest.fixture(params=match_params) 134def pattern_match(request): 135 return map(make_local_path, request.param) 136 137 138mismatch_params = itertools.chain.from_iterable( 139 zip(itertools.repeat(pattern), mismatches) 140 for pattern, matches, mismatches in translate_specs 141) 142 143 144@pytest.fixture(params=mismatch_params) 145def pattern_mismatch(request): 146 return map(make_local_path, request.param) 147 148 149def test_translated_pattern_match(pattern_match): 150 pattern, target = pattern_match 151 assert translate_pattern(pattern).match(target) 152 153 154def test_translated_pattern_mismatch(pattern_mismatch): 155 pattern, target = pattern_mismatch 156 assert not translate_pattern(pattern).match(target) 157 158 159class TempDirTestCase: 160 def setup_method(self, method): 161 self.temp_dir = tempfile.mkdtemp() 162 self.old_cwd = os.getcwd() 163 os.chdir(self.temp_dir) 164 165 def teardown_method(self, method): 166 os.chdir(self.old_cwd) 167 shutil.rmtree(self.temp_dir) 168 169 170class TestManifestTest(TempDirTestCase): 171 def setup_method(self, method): 172 super(TestManifestTest, self).setup_method(method) 173 174 f = open(os.path.join(self.temp_dir, 'setup.py'), 'w') 175 f.write(SETUP_PY) 176 f.close() 177 """ 178 Create a file tree like: 179 - LICENSE 180 - README.rst 181 - testing.rst 182 - .hidden.rst 183 - app/ 184 - __init__.py 185 - a.txt 186 - b.txt 187 - c.rst 188 - static/ 189 - app.js 190 - app.js.map 191 - app.css 192 - app.css.map 193 """ 194 195 for fname in ['README.rst', '.hidden.rst', 'testing.rst', 'LICENSE']: 196 touch(os.path.join(self.temp_dir, fname)) 197 198 # Set up the rest of the test package 199 test_pkg = os.path.join(self.temp_dir, 'app') 200 os.mkdir(test_pkg) 201 for fname in ['__init__.py', 'a.txt', 'b.txt', 'c.rst']: 202 touch(os.path.join(test_pkg, fname)) 203 204 # Some compiled front-end assets to include 205 static = os.path.join(test_pkg, 'static') 206 os.mkdir(static) 207 for fname in ['app.js', 'app.js.map', 'app.css', 'app.css.map']: 208 touch(os.path.join(static, fname)) 209 210 def make_manifest(self, contents): 211 """Write a MANIFEST.in.""" 212 with open(os.path.join(self.temp_dir, 'MANIFEST.in'), 'w') as f: 213 f.write(DALS(contents)) 214 215 def get_files(self): 216 """Run egg_info and get all the files to include, as a set""" 217 dist = Distribution(SETUP_ATTRS) 218 dist.script_name = 'setup.py' 219 cmd = egg_info(dist) 220 cmd.ensure_finalized() 221 222 cmd.run() 223 224 return set(cmd.filelist.files) 225 226 def test_no_manifest(self): 227 """Check a missing MANIFEST.in includes only the standard files.""" 228 assert (default_files - set(['MANIFEST.in'])) == self.get_files() 229 230 def test_empty_files(self): 231 """Check an empty MANIFEST.in includes only the standard files.""" 232 self.make_manifest("") 233 assert default_files == self.get_files() 234 235 def test_include(self): 236 """Include extra rst files in the project root.""" 237 self.make_manifest("include *.rst") 238 files = default_files | set([ 239 'testing.rst', '.hidden.rst']) 240 assert files == self.get_files() 241 242 def test_exclude(self): 243 """Include everything in app/ except the text files""" 244 ml = make_local_path 245 self.make_manifest( 246 """ 247 include app/* 248 exclude app/*.txt 249 """) 250 files = default_files | set([ml('app/c.rst')]) 251 assert files == self.get_files() 252 253 def test_include_multiple(self): 254 """Include with multiple patterns.""" 255 ml = make_local_path 256 self.make_manifest("include app/*.txt app/static/*") 257 files = default_files | set([ 258 ml('app/a.txt'), ml('app/b.txt'), 259 ml('app/static/app.js'), ml('app/static/app.js.map'), 260 ml('app/static/app.css'), ml('app/static/app.css.map')]) 261 assert files == self.get_files() 262 263 def test_graft(self): 264 """Include the whole app/static/ directory.""" 265 ml = make_local_path 266 self.make_manifest("graft app/static") 267 files = default_files | set([ 268 ml('app/static/app.js'), ml('app/static/app.js.map'), 269 ml('app/static/app.css'), ml('app/static/app.css.map')]) 270 assert files == self.get_files() 271 272 def test_graft_glob_syntax(self): 273 """Include the whole app/static/ directory.""" 274 ml = make_local_path 275 self.make_manifest("graft */static") 276 files = default_files | set([ 277 ml('app/static/app.js'), ml('app/static/app.js.map'), 278 ml('app/static/app.css'), ml('app/static/app.css.map')]) 279 assert files == self.get_files() 280 281 def test_graft_global_exclude(self): 282 """Exclude all *.map files in the project.""" 283 ml = make_local_path 284 self.make_manifest( 285 """ 286 graft app/static 287 global-exclude *.map 288 """) 289 files = default_files | set([ 290 ml('app/static/app.js'), ml('app/static/app.css')]) 291 assert files == self.get_files() 292 293 def test_global_include(self): 294 """Include all *.rst, *.js, and *.css files in the whole tree.""" 295 ml = make_local_path 296 self.make_manifest( 297 """ 298 global-include *.rst *.js *.css 299 """) 300 files = default_files | set([ 301 '.hidden.rst', 'testing.rst', ml('app/c.rst'), 302 ml('app/static/app.js'), ml('app/static/app.css')]) 303 assert files == self.get_files() 304 305 def test_graft_prune(self): 306 """Include all files in app/, except for the whole app/static/ dir.""" 307 ml = make_local_path 308 self.make_manifest( 309 """ 310 graft app 311 prune app/static 312 """) 313 files = default_files | set([ 314 ml('app/a.txt'), ml('app/b.txt'), ml('app/c.rst')]) 315 assert files == self.get_files() 316 317 318class TestFileListTest(TempDirTestCase): 319 """ 320 A copy of the relevant bits of distutils/tests/test_filelist.py, 321 to ensure setuptools' version of FileList keeps parity with distutils. 322 """ 323 324 def setup_method(self, method): 325 super(TestFileListTest, self).setup_method(method) 326 self.threshold = log.set_threshold(log.FATAL) 327 self._old_log = log.Log._log 328 log.Log._log = self._log 329 self.logs = [] 330 331 def teardown_method(self, method): 332 log.set_threshold(self.threshold) 333 log.Log._log = self._old_log 334 super(TestFileListTest, self).teardown_method(method) 335 336 def _log(self, level, msg, args): 337 if level not in (log.DEBUG, log.INFO, log.WARN, log.ERROR, log.FATAL): 338 raise ValueError('%s wrong log level' % str(level)) 339 self.logs.append((level, msg, args)) 340 341 def get_logs(self, *levels): 342 def _format(msg, args): 343 if len(args) == 0: 344 return msg 345 return msg % args 346 return [_format(msg, args) for level, msg, args 347 in self.logs if level in levels] 348 349 def clear_logs(self): 350 self.logs = [] 351 352 def assertNoWarnings(self): 353 assert self.get_logs(log.WARN) == [] 354 self.clear_logs() 355 356 def assertWarnings(self): 357 assert len(self.get_logs(log.WARN)) > 0 358 self.clear_logs() 359 360 def make_files(self, files): 361 for file in files: 362 file = os.path.join(self.temp_dir, file) 363 dirname, basename = os.path.split(file) 364 os.makedirs(dirname, exist_ok=True) 365 open(file, 'w').close() 366 367 def test_process_template_line(self): 368 # testing all MANIFEST.in template patterns 369 file_list = FileList() 370 ml = make_local_path 371 372 # simulated file list 373 self.make_files([ 374 'foo.tmp', 'ok', 'xo', 'four.txt', 375 'buildout.cfg', 376 # filelist does not filter out VCS directories, 377 # it's sdist that does 378 ml('.hg/last-message.txt'), 379 ml('global/one.txt'), 380 ml('global/two.txt'), 381 ml('global/files.x'), 382 ml('global/here.tmp'), 383 ml('f/o/f.oo'), 384 ml('dir/graft-one'), 385 ml('dir/dir2/graft2'), 386 ml('dir3/ok'), 387 ml('dir3/sub/ok.txt'), 388 ]) 389 390 MANIFEST_IN = DALS("""\ 391 include ok 392 include xo 393 exclude xo 394 include foo.tmp 395 include buildout.cfg 396 global-include *.x 397 global-include *.txt 398 global-exclude *.tmp 399 recursive-include f *.oo 400 recursive-exclude global *.x 401 graft dir 402 prune dir3 403 """) 404 405 for line in MANIFEST_IN.split('\n'): 406 if not line: 407 continue 408 file_list.process_template_line(line) 409 410 wanted = [ 411 'buildout.cfg', 412 'four.txt', 413 'ok', 414 ml('.hg/last-message.txt'), 415 ml('dir/graft-one'), 416 ml('dir/dir2/graft2'), 417 ml('f/o/f.oo'), 418 ml('global/one.txt'), 419 ml('global/two.txt'), 420 ] 421 422 file_list.sort() 423 assert file_list.files == wanted 424 425 def test_exclude_pattern(self): 426 # return False if no match 427 file_list = FileList() 428 assert not file_list.exclude_pattern('*.py') 429 430 # return True if files match 431 file_list = FileList() 432 file_list.files = ['a.py', 'b.py'] 433 assert file_list.exclude_pattern('*.py') 434 435 # test excludes 436 file_list = FileList() 437 file_list.files = ['a.py', 'a.txt'] 438 file_list.exclude_pattern('*.py') 439 file_list.sort() 440 assert file_list.files == ['a.txt'] 441 442 def test_include_pattern(self): 443 # return False if no match 444 file_list = FileList() 445 self.make_files([]) 446 assert not file_list.include_pattern('*.py') 447 448 # return True if files match 449 file_list = FileList() 450 self.make_files(['a.py', 'b.txt']) 451 assert file_list.include_pattern('*.py') 452 453 # test * matches all files 454 file_list = FileList() 455 self.make_files(['a.py', 'b.txt']) 456 file_list.include_pattern('*') 457 file_list.sort() 458 assert file_list.files == ['a.py', 'b.txt'] 459 460 def test_process_template_line_invalid(self): 461 # invalid lines 462 file_list = FileList() 463 for action in ('include', 'exclude', 'global-include', 464 'global-exclude', 'recursive-include', 465 'recursive-exclude', 'graft', 'prune', 'blarg'): 466 try: 467 file_list.process_template_line(action) 468 except DistutilsTemplateError: 469 pass 470 except Exception: 471 assert False, "Incorrect error thrown" 472 else: 473 assert False, "Should have thrown an error" 474 475 def test_include(self): 476 ml = make_local_path 477 # include 478 file_list = FileList() 479 self.make_files(['a.py', 'b.txt', ml('d/c.py')]) 480 481 file_list.process_template_line('include *.py') 482 file_list.sort() 483 assert file_list.files == ['a.py'] 484 self.assertNoWarnings() 485 486 file_list.process_template_line('include *.rb') 487 file_list.sort() 488 assert file_list.files == ['a.py'] 489 self.assertWarnings() 490 491 def test_exclude(self): 492 ml = make_local_path 493 # exclude 494 file_list = FileList() 495 file_list.files = ['a.py', 'b.txt', ml('d/c.py')] 496 497 file_list.process_template_line('exclude *.py') 498 file_list.sort() 499 assert file_list.files == ['b.txt', ml('d/c.py')] 500 self.assertNoWarnings() 501 502 file_list.process_template_line('exclude *.rb') 503 file_list.sort() 504 assert file_list.files == ['b.txt', ml('d/c.py')] 505 self.assertWarnings() 506 507 def test_global_include(self): 508 ml = make_local_path 509 # global-include 510 file_list = FileList() 511 self.make_files(['a.py', 'b.txt', ml('d/c.py')]) 512 513 file_list.process_template_line('global-include *.py') 514 file_list.sort() 515 assert file_list.files == ['a.py', ml('d/c.py')] 516 self.assertNoWarnings() 517 518 file_list.process_template_line('global-include *.rb') 519 file_list.sort() 520 assert file_list.files == ['a.py', ml('d/c.py')] 521 self.assertWarnings() 522 523 def test_global_exclude(self): 524 ml = make_local_path 525 # global-exclude 526 file_list = FileList() 527 file_list.files = ['a.py', 'b.txt', ml('d/c.py')] 528 529 file_list.process_template_line('global-exclude *.py') 530 file_list.sort() 531 assert file_list.files == ['b.txt'] 532 self.assertNoWarnings() 533 534 file_list.process_template_line('global-exclude *.rb') 535 file_list.sort() 536 assert file_list.files == ['b.txt'] 537 self.assertWarnings() 538 539 def test_recursive_include(self): 540 ml = make_local_path 541 # recursive-include 542 file_list = FileList() 543 self.make_files(['a.py', ml('d/b.py'), ml('d/c.txt'), ml('d/d/e.py')]) 544 545 file_list.process_template_line('recursive-include d *.py') 546 file_list.sort() 547 assert file_list.files == [ml('d/b.py'), ml('d/d/e.py')] 548 self.assertNoWarnings() 549 550 file_list.process_template_line('recursive-include e *.py') 551 file_list.sort() 552 assert file_list.files == [ml('d/b.py'), ml('d/d/e.py')] 553 self.assertWarnings() 554 555 def test_recursive_exclude(self): 556 ml = make_local_path 557 # recursive-exclude 558 file_list = FileList() 559 file_list.files = ['a.py', ml('d/b.py'), ml('d/c.txt'), ml('d/d/e.py')] 560 561 file_list.process_template_line('recursive-exclude d *.py') 562 file_list.sort() 563 assert file_list.files == ['a.py', ml('d/c.txt')] 564 self.assertNoWarnings() 565 566 file_list.process_template_line('recursive-exclude e *.py') 567 file_list.sort() 568 assert file_list.files == ['a.py', ml('d/c.txt')] 569 self.assertWarnings() 570 571 def test_graft(self): 572 ml = make_local_path 573 # graft 574 file_list = FileList() 575 self.make_files(['a.py', ml('d/b.py'), ml('d/d/e.py'), ml('f/f.py')]) 576 577 file_list.process_template_line('graft d') 578 file_list.sort() 579 assert file_list.files == [ml('d/b.py'), ml('d/d/e.py')] 580 self.assertNoWarnings() 581 582 file_list.process_template_line('graft e') 583 file_list.sort() 584 assert file_list.files == [ml('d/b.py'), ml('d/d/e.py')] 585 self.assertWarnings() 586 587 def test_prune(self): 588 ml = make_local_path 589 # prune 590 file_list = FileList() 591 file_list.files = ['a.py', ml('d/b.py'), ml('d/d/e.py'), ml('f/f.py')] 592 593 file_list.process_template_line('prune d') 594 file_list.sort() 595 assert file_list.files == ['a.py', ml('f/f.py')] 596 self.assertNoWarnings() 597 598 file_list.process_template_line('prune e') 599 file_list.sort() 600 assert file_list.files == ['a.py', ml('f/f.py')] 601 self.assertWarnings() 602