codekingpro/portable-devtools
114k
1"""sdist tests"""2 3from __future__ import annotations4 5import contextlib6import io7import itertools8import logging9import os10import shutil11import sys12import tempfile13 14import pytest15 16from setuptools.command.egg_info import FileList, egg_info, translate_pattern17from setuptools.dist import Distribution18from setuptools.tests.textwrap import DALS19 20from distutils import log21from distutils.errors import DistutilsTemplateError22 23IS_PYPY = '__pypy__' in sys.builtin_module_names24 25 26def make_local_path(s):27 """Converts '/' in a string to os.sep"""28 return s.replace('/', os.sep)29 30 31SETUP_ATTRS = {32 'name': 'app',33 'version': '0.0',34 'packages': ['app'],35}36 37SETUP_PY = f"""\38from setuptools import setup39 40setup(**{SETUP_ATTRS!r})41"""42 43 44@contextlib.contextmanager45def quiet():46 old_stdout, old_stderr = sys.stdout, sys.stderr47 sys.stdout, sys.stderr = io.StringIO(), io.StringIO()48 try:49 yield50 finally:51 sys.stdout, sys.stderr = old_stdout, old_stderr52 53 54def touch(filename):55 open(filename, 'wb').close()56 57 58# The set of files always in the manifest, including all files in the59# .egg-info directory60default_files = frozenset(61 map(62 make_local_path,63 [64 'README.rst',65 'MANIFEST.in',66 'setup.py',67 'app.egg-info/PKG-INFO',68 'app.egg-info/SOURCES.txt',69 'app.egg-info/dependency_links.txt',70 'app.egg-info/top_level.txt',71 'app/__init__.py',72 ],73 )74)75 76 77translate_specs: list[tuple[str, list[str], list[str]]] = [78 ('foo', ['foo'], ['bar', 'foobar']),79 ('foo/bar', ['foo/bar'], ['foo/bar/baz', './foo/bar', 'foo']),80 # Glob matching81 ('*.txt', ['foo.txt', 'bar.txt'], ['foo/foo.txt']),82 ('dir/*.txt', ['dir/foo.txt', 'dir/bar.txt', 'dir/.txt'], ['notdir/foo.txt']),83 ('*/*.py', ['bin/start.py'], []),84 ('docs/page-?.txt', ['docs/page-9.txt'], ['docs/page-10.txt']),85 # Globstars change what they mean depending upon where they are86 (87 'foo/**/bar',88 ['foo/bing/bar', 'foo/bing/bang/bar', 'foo/bar'],89 ['foo/abar'],90 ),91 (92 'foo/**',93 ['foo/bar/bing.py', 'foo/x'],94 ['/foo/x'],95 ),96 (97 '**',98 ['x', 'abc/xyz', '@nything'],99 [],100 ),101 # Character classes102 (103 'pre[one]post',104 ['preopost', 'prenpost', 'preepost'],105 ['prepost', 'preonepost'],106 ),107 (108 'hello[!one]world',109 ['helloxworld', 'helloyworld'],110 ['hellooworld', 'helloworld', 'hellooneworld'],111 ),112 (113 '[]one].txt',114 ['o.txt', '].txt', 'e.txt'],115 ['one].txt'],116 ),117 (118 'foo[!]one]bar',119 ['fooybar'],120 ['foo]bar', 'fooobar', 'fooebar'],121 ),122]123"""124A spec of inputs for 'translate_pattern' and matches and mismatches125for that input.126"""127 128match_params = itertools.chain.from_iterable(129 zip(itertools.repeat(pattern), matches)130 for pattern, matches, mismatches in translate_specs131)132 133 134@pytest.fixture(params=match_params)135def pattern_match(request):136 return map(make_local_path, request.param)137 138 139mismatch_params = itertools.chain.from_iterable(140 zip(itertools.repeat(pattern), mismatches)141 for pattern, matches, mismatches in translate_specs142)143 144 145@pytest.fixture(params=mismatch_params)146def pattern_mismatch(request):147 return map(make_local_path, request.param)148 149 150def test_translated_pattern_match(pattern_match):151 pattern, target = pattern_match152 assert translate_pattern(pattern).match(target)153 154 155def test_translated_pattern_mismatch(pattern_mismatch):156 pattern, target = pattern_mismatch157 assert not translate_pattern(pattern).match(target)158 159 160class TempDirTestCase:161 def setup_method(self, method):162 self.temp_dir = tempfile.mkdtemp()163 self.old_cwd = os.getcwd()164 os.chdir(self.temp_dir)165 166 def teardown_method(self, method):167 os.chdir(self.old_cwd)168 shutil.rmtree(self.temp_dir)169 170 171class TestManifestTest(TempDirTestCase):172 def setup_method(self, method):173 super().setup_method(method)174 175 f = open(os.path.join(self.temp_dir, 'setup.py'), 'w', encoding="utf-8")176 f.write(SETUP_PY)177 f.close()178 """179 Create a file tree like:180 - LICENSE181 - README.rst182 - testing.rst183 - .hidden.rst184 - app/185 - __init__.py186 - a.txt187 - b.txt188 - c.rst189 - static/190 - app.js191 - app.js.map192 - app.css193 - app.css.map194 """195 196 for fname in ['README.rst', '.hidden.rst', 'testing.rst', 'LICENSE']:197 touch(os.path.join(self.temp_dir, fname))198 199 # Set up the rest of the test package200 test_pkg = os.path.join(self.temp_dir, 'app')201 os.mkdir(test_pkg)202 for fname in ['__init__.py', 'a.txt', 'b.txt', 'c.rst']:203 touch(os.path.join(test_pkg, fname))204 205 # Some compiled front-end assets to include206 static = os.path.join(test_pkg, 'static')207 os.mkdir(static)208 for fname in ['app.js', 'app.js.map', 'app.css', 'app.css.map']:209 touch(os.path.join(static, fname))210 211 def make_manifest(self, contents):212 """Write a MANIFEST.in."""213 manifest = os.path.join(self.temp_dir, 'MANIFEST.in')214 with open(manifest, 'w', encoding="utf-8") as f:215 f.write(DALS(contents))216 217 def get_files(self):218 """Run egg_info and get all the files to include, as a set"""219 dist = Distribution(SETUP_ATTRS)220 dist.script_name = 'setup.py'221 cmd = egg_info(dist)222 cmd.ensure_finalized()223 224 cmd.run()225 226 return set(cmd.filelist.files)227 228 def test_no_manifest(self):229 """Check a missing MANIFEST.in includes only the standard files."""230 assert (default_files - set(['MANIFEST.in'])) == self.get_files()231 232 def test_empty_files(self):233 """Check an empty MANIFEST.in includes only the standard files."""234 self.make_manifest("")235 assert default_files == self.get_files()236 237 def test_include(self):238 """Include extra rst files in the project root."""239 self.make_manifest("include *.rst")240 files = default_files | set(['testing.rst', '.hidden.rst'])241 assert files == self.get_files()242 243 def test_exclude(self):244 """Include everything in app/ except the text files"""245 ml = make_local_path246 self.make_manifest(247 """248 include app/*249 exclude app/*.txt250 """251 )252 files = default_files | set([ml('app/c.rst')])253 assert files == self.get_files()254 255 def test_include_multiple(self):256 """Include with multiple patterns."""257 ml = make_local_path258 self.make_manifest("include app/*.txt app/static/*")259 files = default_files | set([260 ml('app/a.txt'),261 ml('app/b.txt'),262 ml('app/static/app.js'),263 ml('app/static/app.js.map'),264 ml('app/static/app.css'),265 ml('app/static/app.css.map'),266 ])267 assert files == self.get_files()268 269 def test_graft(self):270 """Include the whole app/static/ directory."""271 ml = make_local_path272 self.make_manifest("graft app/static")273 files = default_files | set([274 ml('app/static/app.js'),275 ml('app/static/app.js.map'),276 ml('app/static/app.css'),277 ml('app/static/app.css.map'),278 ])279 assert files == self.get_files()280 281 def test_graft_glob_syntax(self):282 """Include the whole app/static/ directory."""283 ml = make_local_path284 self.make_manifest("graft */static")285 files = default_files | set([286 ml('app/static/app.js'),287 ml('app/static/app.js.map'),288 ml('app/static/app.css'),289 ml('app/static/app.css.map'),290 ])291 assert files == self.get_files()292 293 def test_graft_global_exclude(self):294 """Exclude all *.map files in the project."""295 ml = make_local_path296 self.make_manifest(297 """298 graft app/static299 global-exclude *.map300 """301 )302 files = default_files | set([ml('app/static/app.js'), ml('app/static/app.css')])303 assert files == self.get_files()304 305 def test_global_include(self):306 """Include all *.rst, *.js, and *.css files in the whole tree."""307 ml = make_local_path308 self.make_manifest(309 """310 global-include *.rst *.js *.css311 """312 )313 files = default_files | set([314 '.hidden.rst',315 'testing.rst',316 ml('app/c.rst'),317 ml('app/static/app.js'),318 ml('app/static/app.css'),319 ])320 assert files == self.get_files()321 322 def test_graft_prune(self):323 """Include all files in app/, except for the whole app/static/ dir."""324 ml = make_local_path325 self.make_manifest(326 """327 graft app328 prune app/static329 """330 )331 files = default_files | set([ml('app/a.txt'), ml('app/b.txt'), ml('app/c.rst')])332 assert files == self.get_files()333 334 335class TestFileListTest(TempDirTestCase):336 """337 A copy of the relevant bits of distutils/tests/test_filelist.py,338 to ensure setuptools' version of FileList keeps parity with distutils.339 """340 341 @pytest.fixture(autouse=os.getenv("SETUPTOOLS_USE_DISTUTILS") == "stdlib")342 def _compat_record_logs(self, monkeypatch, caplog):343 """Account for stdlib compatibility"""344 345 def _log(_logger, level, msg, args):346 exc = sys.exc_info()347 rec = logging.LogRecord("distutils", level, "", 0, msg, args, exc)348 caplog.records.append(rec)349 350 monkeypatch.setattr(log.Log, "_log", _log)351 352 def get_records(self, caplog, *levels):353 return [r for r in caplog.records if r.levelno in levels]354 355 def assertNoWarnings(self, caplog):356 assert self.get_records(caplog, log.WARN) == []357 caplog.clear()358 359 def assertWarnings(self, caplog):360 if IS_PYPY and not caplog.records:361 pytest.xfail("caplog checks may not work well in PyPy")362 else:363 assert len(self.get_records(caplog, log.WARN)) > 0364 caplog.clear()365 366 def make_files(self, files):367 for file in files:368 file = os.path.join(self.temp_dir, file)369 dirname, _basename = os.path.split(file)370 os.makedirs(dirname, exist_ok=True)371 touch(file)372 373 def test_process_template_line(self):374 # testing all MANIFEST.in template patterns375 file_list = FileList()376 ml = make_local_path377 378 # simulated file list379 self.make_files([380 'foo.tmp',381 'ok',382 'xo',383 'four.txt',384 'buildout.cfg',385 # filelist does not filter out VCS directories,386 # it's sdist that does387 ml('.hg/last-message.txt'),388 ml('global/one.txt'),389 ml('global/two.txt'),390 ml('global/files.x'),391 ml('global/here.tmp'),392 ml('f/o/f.oo'),393 ml('dir/graft-one'),394 ml('dir/dir2/graft2'),395 ml('dir3/ok'),396 ml('dir3/sub/ok.txt'),397 ])398 399 MANIFEST_IN = DALS(400 """\401 include ok402 include xo403 exclude xo404 include foo.tmp405 include buildout.cfg406 global-include *.x407 global-include *.txt408 global-exclude *.tmp409 recursive-include f *.oo410 recursive-exclude global *.x411 graft dir412 prune dir3413 """414 )415 416 for line in MANIFEST_IN.split('\n'):417 if not line:418 continue419 file_list.process_template_line(line)420 421 wanted = [422 'buildout.cfg',423 'four.txt',424 'ok',425 ml('.hg/last-message.txt'),426 ml('dir/graft-one'),427 ml('dir/dir2/graft2'),428 ml('f/o/f.oo'),429 ml('global/one.txt'),430 ml('global/two.txt'),431 ]432 433 file_list.sort()434 assert file_list.files == wanted435 436 def test_exclude_pattern(self):437 # return False if no match438 file_list = FileList()439 assert not file_list.exclude_pattern('*.py')440 441 # return True if files match442 file_list = FileList()443 file_list.files = ['a.py', 'b.py']444 assert file_list.exclude_pattern('*.py')445 446 # test excludes447 file_list = FileList()448 file_list.files = ['a.py', 'a.txt']449 file_list.exclude_pattern('*.py')450 file_list.sort()451 assert file_list.files == ['a.txt']452 453 def test_include_pattern(self):454 # return False if no match455 file_list = FileList()456 self.make_files([])457 assert not file_list.include_pattern('*.py')458 459 # return True if files match460 file_list = FileList()461 self.make_files(['a.py', 'b.txt'])462 assert file_list.include_pattern('*.py')463 464 # test * matches all files465 file_list = FileList()466 self.make_files(['a.py', 'b.txt'])467 file_list.include_pattern('*')468 file_list.sort()469 assert file_list.files == ['a.py', 'b.txt']470 471 def test_process_template_line_invalid(self):472 # invalid lines473 file_list = FileList()474 for action in (475 'include',476 'exclude',477 'global-include',478 'global-exclude',479 'recursive-include',480 'recursive-exclude',481 'graft',482 'prune',483 'blarg',484 ):485 with pytest.raises(DistutilsTemplateError):486 file_list.process_template_line(action)487 488 def test_include(self, caplog):489 caplog.set_level(logging.DEBUG)490 ml = make_local_path491 # include492 file_list = FileList()493 self.make_files(['a.py', 'b.txt', ml('d/c.py')])494 495 file_list.process_template_line('include *.py')496 file_list.sort()497 assert file_list.files == ['a.py']498 self.assertNoWarnings(caplog)499 500 file_list.process_template_line('include *.rb')501 file_list.sort()502 assert file_list.files == ['a.py']503 self.assertWarnings(caplog)504 505 def test_exclude(self, caplog):506 caplog.set_level(logging.DEBUG)507 ml = make_local_path508 # exclude509 file_list = FileList()510 file_list.files = ['a.py', 'b.txt', ml('d/c.py')]511 512 file_list.process_template_line('exclude *.py')513 file_list.sort()514 assert file_list.files == ['b.txt', ml('d/c.py')]515 self.assertNoWarnings(caplog)516 517 file_list.process_template_line('exclude *.rb')518 file_list.sort()519 assert file_list.files == ['b.txt', ml('d/c.py')]520 self.assertWarnings(caplog)521 522 def test_global_include(self, caplog):523 caplog.set_level(logging.DEBUG)524 ml = make_local_path525 # global-include526 file_list = FileList()527 self.make_files(['a.py', 'b.txt', ml('d/c.py')])528 529 file_list.process_template_line('global-include *.py')530 file_list.sort()531 assert file_list.files == ['a.py', ml('d/c.py')]532 self.assertNoWarnings(caplog)533 534 file_list.process_template_line('global-include *.rb')535 file_list.sort()536 assert file_list.files == ['a.py', ml('d/c.py')]537 self.assertWarnings(caplog)538 539 def test_global_exclude(self, caplog):540 caplog.set_level(logging.DEBUG)541 ml = make_local_path542 # global-exclude543 file_list = FileList()544 file_list.files = ['a.py', 'b.txt', ml('d/c.py')]545 546 file_list.process_template_line('global-exclude *.py')547 file_list.sort()548 assert file_list.files == ['b.txt']549 self.assertNoWarnings(caplog)550 551 file_list.process_template_line('global-exclude *.rb')552 file_list.sort()553 assert file_list.files == ['b.txt']554 self.assertWarnings(caplog)555 556 def test_recursive_include(self, caplog):557 caplog.set_level(logging.DEBUG)558 ml = make_local_path559 # recursive-include560 file_list = FileList()561 self.make_files(['a.py', ml('d/b.py'), ml('d/c.txt'), ml('d/d/e.py')])562 563 file_list.process_template_line('recursive-include d *.py')564 file_list.sort()565 assert file_list.files == [ml('d/b.py'), ml('d/d/e.py')]566 self.assertNoWarnings(caplog)567 568 file_list.process_template_line('recursive-include e *.py')569 file_list.sort()570 assert file_list.files == [ml('d/b.py'), ml('d/d/e.py')]571 self.assertWarnings(caplog)572 573 def test_recursive_exclude(self, caplog):574 caplog.set_level(logging.DEBUG)575 ml = make_local_path576 # recursive-exclude577 file_list = FileList()578 file_list.files = ['a.py', ml('d/b.py'), ml('d/c.txt'), ml('d/d/e.py')]579 580 file_list.process_template_line('recursive-exclude d *.py')581 file_list.sort()582 assert file_list.files == ['a.py', ml('d/c.txt')]583 self.assertNoWarnings(caplog)584 585 file_list.process_template_line('recursive-exclude e *.py')586 file_list.sort()587 assert file_list.files == ['a.py', ml('d/c.txt')]588 self.assertWarnings(caplog)589 590 def test_graft(self, caplog):591 caplog.set_level(logging.DEBUG)592 ml = make_local_path593 # graft594 file_list = FileList()595 self.make_files(['a.py', ml('d/b.py'), ml('d/d/e.py'), ml('f/f.py')])596 597 file_list.process_template_line('graft d')598 file_list.sort()599 assert file_list.files == [ml('d/b.py'), ml('d/d/e.py')]600 self.assertNoWarnings(caplog)601 602 file_list.process_template_line('graft e')603 file_list.sort()604 assert file_list.files == [ml('d/b.py'), ml('d/d/e.py')]605 self.assertWarnings(caplog)606 607 def test_prune(self, caplog):608 caplog.set_level(logging.DEBUG)609 ml = make_local_path610 # prune611 file_list = FileList()612 file_list.files = ['a.py', ml('d/b.py'), ml('d/d/e.py'), ml('f/f.py')]613 614 file_list.process_template_line('prune d')615 file_list.sort()616 assert file_list.files == ['a.py', ml('f/f.py')]617 self.assertNoWarnings(caplog)618 619 file_list.process_template_line('prune e')620 file_list.sort()621 assert file_list.files == ['a.py', ml('f/f.py')]622 self.assertWarnings(caplog)623 