test_manifest.py 18 KB

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