test_sdist.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470
  1. """Tests for distutils.command.sdist."""
  2. import os
  3. import pathlib
  4. import shutil # noqa: F401
  5. import tarfile
  6. import zipfile
  7. from distutils.archive_util import ARCHIVE_FORMATS
  8. from distutils.command.sdist import sdist, show_formats
  9. from distutils.core import Distribution
  10. from distutils.errors import DistutilsOptionError
  11. from distutils.filelist import FileList
  12. from os.path import join
  13. from textwrap import dedent
  14. import jaraco.path
  15. import path
  16. import pytest
  17. from more_itertools import ilen
  18. from . import support
  19. from .unix_compat import grp, pwd, require_uid_0, require_unix_id
  20. SETUP_PY = """
  21. from distutils.core import setup
  22. import somecode
  23. setup(name='fake')
  24. """
  25. MANIFEST = """\
  26. # file GENERATED by distutils, do NOT edit
  27. README
  28. buildout.cfg
  29. inroot.txt
  30. setup.py
  31. data%(sep)sdata.dt
  32. scripts%(sep)sscript.py
  33. some%(sep)sfile.txt
  34. some%(sep)sother_file.txt
  35. somecode%(sep)s__init__.py
  36. somecode%(sep)sdoc.dat
  37. somecode%(sep)sdoc.txt
  38. """
  39. @pytest.fixture(autouse=True)
  40. def project_dir(request, distutils_managed_tempdir):
  41. self = request.instance
  42. self.tmp_dir = self.mkdtemp()
  43. jaraco.path.build(
  44. {
  45. 'somecode': {
  46. '__init__.py': '#',
  47. },
  48. 'README': 'xxx',
  49. 'setup.py': SETUP_PY,
  50. },
  51. self.tmp_dir,
  52. )
  53. with path.Path(self.tmp_dir):
  54. yield
  55. def clean_lines(filepath):
  56. with pathlib.Path(filepath).open(encoding='utf-8') as f:
  57. yield from filter(None, map(str.strip, f))
  58. class TestSDist(support.TempdirManager):
  59. def get_cmd(self, metadata=None):
  60. """Returns a cmd"""
  61. if metadata is None:
  62. metadata = {
  63. 'name': 'ns.fake--pkg',
  64. 'version': '1.0',
  65. 'url': 'xxx',
  66. 'author': 'xxx',
  67. 'author_email': 'xxx',
  68. }
  69. dist = Distribution(metadata)
  70. dist.script_name = 'setup.py'
  71. dist.packages = ['somecode']
  72. dist.include_package_data = True
  73. cmd = sdist(dist)
  74. cmd.dist_dir = 'dist'
  75. return dist, cmd
  76. @pytest.mark.usefixtures('needs_zlib')
  77. def test_prune_file_list(self):
  78. # this test creates a project with some VCS dirs and an NFS rename
  79. # file, then launches sdist to check they get pruned on all systems
  80. # creating VCS directories with some files in them
  81. os.mkdir(join(self.tmp_dir, 'somecode', '.svn'))
  82. self.write_file((self.tmp_dir, 'somecode', '.svn', 'ok.py'), 'xxx')
  83. os.mkdir(join(self.tmp_dir, 'somecode', '.hg'))
  84. self.write_file((self.tmp_dir, 'somecode', '.hg', 'ok'), 'xxx')
  85. os.mkdir(join(self.tmp_dir, 'somecode', '.git'))
  86. self.write_file((self.tmp_dir, 'somecode', '.git', 'ok'), 'xxx')
  87. self.write_file((self.tmp_dir, 'somecode', '.nfs0001'), 'xxx')
  88. # now building a sdist
  89. dist, cmd = self.get_cmd()
  90. # zip is available universally
  91. # (tar might not be installed under win32)
  92. cmd.formats = ['zip']
  93. cmd.ensure_finalized()
  94. cmd.run()
  95. # now let's check what we have
  96. dist_folder = join(self.tmp_dir, 'dist')
  97. files = os.listdir(dist_folder)
  98. assert files == ['ns_fake_pkg-1.0.zip']
  99. zip_file = zipfile.ZipFile(join(dist_folder, 'ns_fake_pkg-1.0.zip'))
  100. try:
  101. content = zip_file.namelist()
  102. finally:
  103. zip_file.close()
  104. # making sure everything has been pruned correctly
  105. expected = [
  106. '',
  107. 'PKG-INFO',
  108. 'README',
  109. 'setup.py',
  110. 'somecode/',
  111. 'somecode/__init__.py',
  112. ]
  113. assert sorted(content) == ['ns_fake_pkg-1.0/' + x for x in expected]
  114. @pytest.mark.usefixtures('needs_zlib')
  115. @pytest.mark.skipif("not shutil.which('tar')")
  116. @pytest.mark.skipif("not shutil.which('gzip')")
  117. def test_make_distribution(self):
  118. # now building a sdist
  119. dist, cmd = self.get_cmd()
  120. # creating a gztar then a tar
  121. cmd.formats = ['gztar', 'tar']
  122. cmd.ensure_finalized()
  123. cmd.run()
  124. # making sure we have two files
  125. dist_folder = join(self.tmp_dir, 'dist')
  126. result = os.listdir(dist_folder)
  127. result.sort()
  128. assert result == ['ns_fake_pkg-1.0.tar', 'ns_fake_pkg-1.0.tar.gz']
  129. os.remove(join(dist_folder, 'ns_fake_pkg-1.0.tar'))
  130. os.remove(join(dist_folder, 'ns_fake_pkg-1.0.tar.gz'))
  131. # now trying a tar then a gztar
  132. cmd.formats = ['tar', 'gztar']
  133. cmd.ensure_finalized()
  134. cmd.run()
  135. result = os.listdir(dist_folder)
  136. result.sort()
  137. assert result == ['ns_fake_pkg-1.0.tar', 'ns_fake_pkg-1.0.tar.gz']
  138. @pytest.mark.usefixtures('needs_zlib')
  139. def test_add_defaults(self):
  140. # https://bugs.python.org/issue2279
  141. # add_default should also include
  142. # data_files and package_data
  143. dist, cmd = self.get_cmd()
  144. # filling data_files by pointing files
  145. # in package_data
  146. dist.package_data = {'': ['*.cfg', '*.dat'], 'somecode': ['*.txt']}
  147. self.write_file((self.tmp_dir, 'somecode', 'doc.txt'), '#')
  148. self.write_file((self.tmp_dir, 'somecode', 'doc.dat'), '#')
  149. # adding some data in data_files
  150. data_dir = join(self.tmp_dir, 'data')
  151. os.mkdir(data_dir)
  152. self.write_file((data_dir, 'data.dt'), '#')
  153. some_dir = join(self.tmp_dir, 'some')
  154. os.mkdir(some_dir)
  155. # make sure VCS directories are pruned (#14004)
  156. hg_dir = join(self.tmp_dir, '.hg')
  157. os.mkdir(hg_dir)
  158. self.write_file((hg_dir, 'last-message.txt'), '#')
  159. # a buggy regex used to prevent this from working on windows (#6884)
  160. self.write_file((self.tmp_dir, 'buildout.cfg'), '#')
  161. self.write_file((self.tmp_dir, 'inroot.txt'), '#')
  162. self.write_file((some_dir, 'file.txt'), '#')
  163. self.write_file((some_dir, 'other_file.txt'), '#')
  164. dist.data_files = [
  165. ('data', ['data/data.dt', 'buildout.cfg', 'inroot.txt', 'notexisting']),
  166. 'some/file.txt',
  167. 'some/other_file.txt',
  168. ]
  169. # adding a script
  170. script_dir = join(self.tmp_dir, 'scripts')
  171. os.mkdir(script_dir)
  172. self.write_file((script_dir, 'script.py'), '#')
  173. dist.scripts = [join('scripts', 'script.py')]
  174. cmd.formats = ['zip']
  175. cmd.use_defaults = True
  176. cmd.ensure_finalized()
  177. cmd.run()
  178. # now let's check what we have
  179. dist_folder = join(self.tmp_dir, 'dist')
  180. files = os.listdir(dist_folder)
  181. assert files == ['ns_fake_pkg-1.0.zip']
  182. zip_file = zipfile.ZipFile(join(dist_folder, 'ns_fake_pkg-1.0.zip'))
  183. try:
  184. content = zip_file.namelist()
  185. finally:
  186. zip_file.close()
  187. # making sure everything was added
  188. expected = [
  189. '',
  190. 'PKG-INFO',
  191. 'README',
  192. 'buildout.cfg',
  193. 'data/',
  194. 'data/data.dt',
  195. 'inroot.txt',
  196. 'scripts/',
  197. 'scripts/script.py',
  198. 'setup.py',
  199. 'some/',
  200. 'some/file.txt',
  201. 'some/other_file.txt',
  202. 'somecode/',
  203. 'somecode/__init__.py',
  204. 'somecode/doc.dat',
  205. 'somecode/doc.txt',
  206. ]
  207. assert sorted(content) == ['ns_fake_pkg-1.0/' + x for x in expected]
  208. # checking the MANIFEST
  209. manifest = pathlib.Path(self.tmp_dir, 'MANIFEST').read_text(encoding='utf-8')
  210. assert manifest == MANIFEST % {'sep': os.sep}
  211. @staticmethod
  212. def warnings(messages, prefix='warning: '):
  213. return [msg for msg in messages if msg.startswith(prefix)]
  214. @pytest.mark.usefixtures('needs_zlib')
  215. def test_metadata_check_option(self, caplog):
  216. # testing the `medata-check` option
  217. dist, cmd = self.get_cmd(metadata={})
  218. # this should raise some warnings !
  219. # with the `check` subcommand
  220. cmd.ensure_finalized()
  221. cmd.run()
  222. assert len(self.warnings(caplog.messages, 'warning: check: ')) == 1
  223. # trying with a complete set of metadata
  224. caplog.clear()
  225. dist, cmd = self.get_cmd()
  226. cmd.ensure_finalized()
  227. cmd.metadata_check = False
  228. cmd.run()
  229. assert len(self.warnings(caplog.messages, 'warning: check: ')) == 0
  230. def test_show_formats(self, capsys):
  231. show_formats()
  232. # the output should be a header line + one line per format
  233. num_formats = len(ARCHIVE_FORMATS.keys())
  234. output = [
  235. line
  236. for line in capsys.readouterr().out.split('\n')
  237. if line.strip().startswith('--formats=')
  238. ]
  239. assert len(output) == num_formats
  240. def test_finalize_options(self):
  241. dist, cmd = self.get_cmd()
  242. cmd.finalize_options()
  243. # default options set by finalize
  244. assert cmd.manifest == 'MANIFEST'
  245. assert cmd.template == 'MANIFEST.in'
  246. assert cmd.dist_dir == 'dist'
  247. # formats has to be a string splitable on (' ', ',') or
  248. # a stringlist
  249. cmd.formats = 1
  250. with pytest.raises(DistutilsOptionError):
  251. cmd.finalize_options()
  252. cmd.formats = ['zip']
  253. cmd.finalize_options()
  254. # formats has to be known
  255. cmd.formats = 'supazipa'
  256. with pytest.raises(DistutilsOptionError):
  257. cmd.finalize_options()
  258. # the following tests make sure there is a nice error message instead
  259. # of a traceback when parsing an invalid manifest template
  260. def _check_template(self, content, caplog):
  261. dist, cmd = self.get_cmd()
  262. os.chdir(self.tmp_dir)
  263. self.write_file('MANIFEST.in', content)
  264. cmd.ensure_finalized()
  265. cmd.filelist = FileList()
  266. cmd.read_template()
  267. assert len(self.warnings(caplog.messages)) == 1
  268. def test_invalid_template_unknown_command(self, caplog):
  269. self._check_template('taunt knights *', caplog)
  270. def test_invalid_template_wrong_arguments(self, caplog):
  271. # this manifest command takes one argument
  272. self._check_template('prune', caplog)
  273. @pytest.mark.skipif("platform.system() != 'Windows'")
  274. def test_invalid_template_wrong_path(self, caplog):
  275. # on Windows, trailing slashes are not allowed
  276. # this used to crash instead of raising a warning: #8286
  277. self._check_template('include examples/', caplog)
  278. @pytest.mark.usefixtures('needs_zlib')
  279. def test_get_file_list(self):
  280. # make sure MANIFEST is recalculated
  281. dist, cmd = self.get_cmd()
  282. # filling data_files by pointing files in package_data
  283. dist.package_data = {'somecode': ['*.txt']}
  284. self.write_file((self.tmp_dir, 'somecode', 'doc.txt'), '#')
  285. cmd.formats = ['gztar']
  286. cmd.ensure_finalized()
  287. cmd.run()
  288. assert ilen(clean_lines(cmd.manifest)) == 5
  289. # adding a file
  290. self.write_file((self.tmp_dir, 'somecode', 'doc2.txt'), '#')
  291. # make sure build_py is reinitialized, like a fresh run
  292. build_py = dist.get_command_obj('build_py')
  293. build_py.finalized = False
  294. build_py.ensure_finalized()
  295. cmd.run()
  296. manifest2 = list(clean_lines(cmd.manifest))
  297. # do we have the new file in MANIFEST ?
  298. assert len(manifest2) == 6
  299. assert 'doc2.txt' in manifest2[-1]
  300. @pytest.mark.usefixtures('needs_zlib')
  301. def test_manifest_marker(self):
  302. # check that autogenerated MANIFESTs have a marker
  303. dist, cmd = self.get_cmd()
  304. cmd.ensure_finalized()
  305. cmd.run()
  306. assert (
  307. next(clean_lines(cmd.manifest))
  308. == '# file GENERATED by distutils, do NOT edit'
  309. )
  310. @pytest.mark.usefixtures('needs_zlib')
  311. def test_manifest_comments(self):
  312. # make sure comments don't cause exceptions or wrong includes
  313. contents = dedent(
  314. """\
  315. # bad.py
  316. #bad.py
  317. good.py
  318. """
  319. )
  320. dist, cmd = self.get_cmd()
  321. cmd.ensure_finalized()
  322. self.write_file((self.tmp_dir, cmd.manifest), contents)
  323. self.write_file((self.tmp_dir, 'good.py'), '# pick me!')
  324. self.write_file((self.tmp_dir, 'bad.py'), "# don't pick me!")
  325. self.write_file((self.tmp_dir, '#bad.py'), "# don't pick me!")
  326. cmd.run()
  327. assert cmd.filelist.files == ['good.py']
  328. @pytest.mark.usefixtures('needs_zlib')
  329. def test_manual_manifest(self):
  330. # check that a MANIFEST without a marker is left alone
  331. dist, cmd = self.get_cmd()
  332. cmd.formats = ['gztar']
  333. cmd.ensure_finalized()
  334. self.write_file((self.tmp_dir, cmd.manifest), 'README.manual')
  335. self.write_file(
  336. (self.tmp_dir, 'README.manual'),
  337. 'This project maintains its MANIFEST file itself.',
  338. )
  339. cmd.run()
  340. assert cmd.filelist.files == ['README.manual']
  341. assert list(clean_lines(cmd.manifest)) == ['README.manual']
  342. archive_name = join(self.tmp_dir, 'dist', 'ns_fake_pkg-1.0.tar.gz')
  343. archive = tarfile.open(archive_name)
  344. try:
  345. filenames = [tarinfo.name for tarinfo in archive]
  346. finally:
  347. archive.close()
  348. assert sorted(filenames) == [
  349. 'ns_fake_pkg-1.0',
  350. 'ns_fake_pkg-1.0/PKG-INFO',
  351. 'ns_fake_pkg-1.0/README.manual',
  352. ]
  353. @pytest.mark.usefixtures('needs_zlib')
  354. @require_unix_id
  355. @require_uid_0
  356. @pytest.mark.skipif("not shutil.which('tar')")
  357. @pytest.mark.skipif("not shutil.which('gzip')")
  358. def test_make_distribution_owner_group(self):
  359. # now building a sdist
  360. dist, cmd = self.get_cmd()
  361. # creating a gztar and specifying the owner+group
  362. cmd.formats = ['gztar']
  363. cmd.owner = pwd.getpwuid(0)[0]
  364. cmd.group = grp.getgrgid(0)[0]
  365. cmd.ensure_finalized()
  366. cmd.run()
  367. # making sure we have the good rights
  368. archive_name = join(self.tmp_dir, 'dist', 'ns_fake_pkg-1.0.tar.gz')
  369. archive = tarfile.open(archive_name)
  370. try:
  371. for member in archive.getmembers():
  372. assert member.uid == 0
  373. assert member.gid == 0
  374. finally:
  375. archive.close()
  376. # building a sdist again
  377. dist, cmd = self.get_cmd()
  378. # creating a gztar
  379. cmd.formats = ['gztar']
  380. cmd.ensure_finalized()
  381. cmd.run()
  382. # making sure we have the good rights
  383. archive_name = join(self.tmp_dir, 'dist', 'ns_fake_pkg-1.0.tar.gz')
  384. archive = tarfile.open(archive_name)
  385. # note that we are not testing the group ownership here
  386. # because, depending on the platforms and the container
  387. # rights (see #7408)
  388. try:
  389. for member in archive.getmembers():
  390. assert member.uid == os.getuid()
  391. finally:
  392. archive.close()