test_egg_info.py 44 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306
  1. from __future__ import annotations
  2. import ast
  3. import glob
  4. import os
  5. import re
  6. import stat
  7. import sys
  8. import time
  9. from pathlib import Path
  10. from unittest import mock
  11. import pytest
  12. from jaraco import path
  13. from setuptools import errors
  14. from setuptools.command.egg_info import egg_info, manifest_maker, write_entries
  15. from setuptools.dist import Distribution
  16. from . import contexts, environment
  17. from .textwrap import DALS
  18. class Environment(str):
  19. pass
  20. @pytest.fixture
  21. def env():
  22. with contexts.tempdir(prefix='setuptools-test.') as env_dir:
  23. env = Environment(env_dir)
  24. os.chmod(env_dir, stat.S_IRWXU)
  25. subs = 'home', 'lib', 'scripts', 'data', 'egg-base'
  26. env.paths = dict((dirname, os.path.join(env_dir, dirname)) for dirname in subs)
  27. list(map(os.mkdir, env.paths.values()))
  28. path.build({
  29. env.paths['home']: {
  30. '.pydistutils.cfg': DALS(
  31. """
  32. [egg_info]
  33. egg-base = {egg-base}
  34. """.format(**env.paths)
  35. )
  36. }
  37. })
  38. yield env
  39. class TestEggInfo:
  40. setup_script = DALS(
  41. """
  42. from setuptools import setup
  43. setup(
  44. name='foo',
  45. py_modules=['hello'],
  46. entry_points={'console_scripts': ['hi = hello.run']},
  47. zip_safe=False,
  48. )
  49. """
  50. )
  51. def _create_project(self):
  52. path.build({
  53. 'setup.py': self.setup_script,
  54. 'hello.py': DALS(
  55. """
  56. def run():
  57. print('hello')
  58. """
  59. ),
  60. })
  61. @staticmethod
  62. def _extract_mv_version(pkg_info_lines: list[str]) -> tuple[int, int]:
  63. version_str = pkg_info_lines[0].split(' ')[1]
  64. major, minor = map(int, version_str.split('.')[:2])
  65. return major, minor
  66. def test_egg_info_save_version_info_setup_empty(self, tmpdir_cwd, env):
  67. """
  68. When the egg_info section is empty or not present, running
  69. save_version_info should add the settings to the setup.cfg
  70. in a deterministic order.
  71. """
  72. setup_cfg = os.path.join(env.paths['home'], 'setup.cfg')
  73. dist = Distribution()
  74. ei = egg_info(dist)
  75. ei.initialize_options()
  76. ei.save_version_info(setup_cfg)
  77. with open(setup_cfg, 'r', encoding="utf-8") as f:
  78. content = f.read()
  79. assert '[egg_info]' in content
  80. assert 'tag_build =' in content
  81. assert 'tag_date = 0' in content
  82. expected_order = (
  83. 'tag_build',
  84. 'tag_date',
  85. )
  86. self._validate_content_order(content, expected_order)
  87. @staticmethod
  88. def _validate_content_order(content, expected):
  89. """
  90. Assert that the strings in expected appear in content
  91. in order.
  92. """
  93. pattern = '.*'.join(expected)
  94. flags = re.MULTILINE | re.DOTALL
  95. assert re.search(pattern, content, flags)
  96. def test_egg_info_save_version_info_setup_defaults(self, tmpdir_cwd, env):
  97. """
  98. When running save_version_info on an existing setup.cfg
  99. with the 'default' values present from a previous run,
  100. the file should remain unchanged.
  101. """
  102. setup_cfg = os.path.join(env.paths['home'], 'setup.cfg')
  103. path.build({
  104. setup_cfg: DALS(
  105. """
  106. [egg_info]
  107. tag_build =
  108. tag_date = 0
  109. """
  110. ),
  111. })
  112. dist = Distribution()
  113. ei = egg_info(dist)
  114. ei.initialize_options()
  115. ei.save_version_info(setup_cfg)
  116. with open(setup_cfg, 'r', encoding="utf-8") as f:
  117. content = f.read()
  118. assert '[egg_info]' in content
  119. assert 'tag_build =' in content
  120. assert 'tag_date = 0' in content
  121. expected_order = (
  122. 'tag_build',
  123. 'tag_date',
  124. )
  125. self._validate_content_order(content, expected_order)
  126. def test_expected_files_produced(self, tmpdir_cwd, env):
  127. self._create_project()
  128. self._run_egg_info_command(tmpdir_cwd, env)
  129. actual = os.listdir('foo.egg-info')
  130. expected = [
  131. 'PKG-INFO',
  132. 'SOURCES.txt',
  133. 'dependency_links.txt',
  134. 'entry_points.txt',
  135. 'not-zip-safe',
  136. 'top_level.txt',
  137. ]
  138. assert sorted(actual) == expected
  139. def test_handling_utime_error(self, tmpdir_cwd, env):
  140. dist = Distribution()
  141. ei = egg_info(dist)
  142. utime_patch = mock.patch('os.utime', side_effect=OSError("TEST"))
  143. mkpath_patch = mock.patch(
  144. 'setuptools.command.egg_info.egg_info.mkpath', return_val=None
  145. )
  146. with utime_patch, mkpath_patch:
  147. import distutils.errors
  148. msg = r"Cannot update time stamp of directory 'None'"
  149. with pytest.raises(distutils.errors.DistutilsFileError, match=msg):
  150. ei.run()
  151. def test_license_is_a_string(self, tmpdir_cwd, env):
  152. setup_config = DALS(
  153. """
  154. [metadata]
  155. name=foo
  156. version=0.0.1
  157. license=file:MIT
  158. """
  159. )
  160. setup_script = DALS(
  161. """
  162. from setuptools import setup
  163. setup()
  164. """
  165. )
  166. path.build({
  167. 'setup.py': setup_script,
  168. 'setup.cfg': setup_config,
  169. })
  170. # This command should fail with a ValueError, but because it's
  171. # currently configured to use a subprocess, the actual traceback
  172. # object is lost and we need to parse it from stderr
  173. with pytest.raises(AssertionError) as exc:
  174. self._run_egg_info_command(tmpdir_cwd, env)
  175. # The only argument to the assertion error should be a traceback
  176. # containing a ValueError
  177. assert 'ValueError' in exc.value.args[0]
  178. def test_rebuilt(self, tmpdir_cwd, env):
  179. """Ensure timestamps are updated when the command is re-run."""
  180. self._create_project()
  181. self._run_egg_info_command(tmpdir_cwd, env)
  182. timestamp_a = os.path.getmtime('foo.egg-info')
  183. # arbitrary sleep just to handle *really* fast systems
  184. time.sleep(0.001)
  185. self._run_egg_info_command(tmpdir_cwd, env)
  186. timestamp_b = os.path.getmtime('foo.egg-info')
  187. assert timestamp_a != timestamp_b
  188. def test_manifest_template_is_read(self, tmpdir_cwd, env):
  189. self._create_project()
  190. path.build({
  191. 'MANIFEST.in': DALS(
  192. """
  193. recursive-include docs *.rst
  194. """
  195. ),
  196. 'docs': {
  197. 'usage.rst': "Run 'hi'",
  198. },
  199. })
  200. self._run_egg_info_command(tmpdir_cwd, env)
  201. egg_info_dir = os.path.join('.', 'foo.egg-info')
  202. sources_txt = os.path.join(egg_info_dir, 'SOURCES.txt')
  203. with open(sources_txt, encoding="utf-8") as f:
  204. assert 'docs/usage.rst' in f.read().split('\n')
  205. def _setup_script_with_requires(self, requires, use_setup_cfg=False):
  206. setup_script = DALS(
  207. """
  208. from setuptools import setup
  209. setup(name='foo', zip_safe=False, %s)
  210. """
  211. ) % ('' if use_setup_cfg else requires)
  212. setup_config = requires if use_setup_cfg else ''
  213. path.build({
  214. 'setup.py': setup_script,
  215. 'setup.cfg': setup_config,
  216. })
  217. mismatch_marker = f"python_version<'{sys.version_info[0]}'"
  218. # Alternate equivalent syntax.
  219. mismatch_marker_alternate = f'python_version < "{sys.version_info[0]}"'
  220. invalid_marker = "<=>++"
  221. class RequiresTestHelper:
  222. @staticmethod
  223. def parametrize(*test_list, **format_dict):
  224. idlist = []
  225. argvalues = []
  226. for test in test_list:
  227. test_params = test.lstrip().split('\n\n', 3)
  228. name_kwargs = test_params.pop(0).split('\n')
  229. if len(name_kwargs) > 1:
  230. val = name_kwargs[1].strip()
  231. install_cmd_kwargs = ast.literal_eval(val)
  232. else:
  233. install_cmd_kwargs = {}
  234. name = name_kwargs[0].strip()
  235. setup_py_requires, setup_cfg_requires, expected_requires = [
  236. DALS(a).format(**format_dict) for a in test_params
  237. ]
  238. for id_, requires, use_cfg in (
  239. (name, setup_py_requires, False),
  240. (name + '_in_setup_cfg', setup_cfg_requires, True),
  241. ):
  242. idlist.append(id_)
  243. marks = ()
  244. if requires.startswith('@xfail\n'):
  245. requires = requires[7:]
  246. marks = pytest.mark.xfail
  247. argvalues.append(
  248. pytest.param(
  249. requires,
  250. use_cfg,
  251. expected_requires,
  252. install_cmd_kwargs,
  253. marks=marks,
  254. )
  255. )
  256. return pytest.mark.parametrize(
  257. (
  258. "requires",
  259. "use_setup_cfg",
  260. "expected_requires",
  261. "install_cmd_kwargs",
  262. ),
  263. argvalues,
  264. ids=idlist,
  265. )
  266. @RequiresTestHelper.parametrize(
  267. # Format of a test:
  268. #
  269. # id
  270. # install_cmd_kwargs [optional]
  271. #
  272. # requires block (when used in setup.py)
  273. #
  274. # requires block (when used in setup.cfg)
  275. #
  276. # expected contents of requires.txt
  277. """
  278. install_requires_deterministic
  279. install_requires=["wheel>=0.5", "pytest"]
  280. [options]
  281. install_requires =
  282. wheel>=0.5
  283. pytest
  284. wheel>=0.5
  285. pytest
  286. """,
  287. """
  288. install_requires_ordered
  289. install_requires=["pytest>=3.0.2,!=10.9999"]
  290. [options]
  291. install_requires =
  292. pytest>=3.0.2,!=10.9999
  293. pytest!=10.9999,>=3.0.2
  294. """,
  295. """
  296. install_requires_with_marker
  297. install_requires=["barbazquux;{mismatch_marker}"],
  298. [options]
  299. install_requires =
  300. barbazquux; {mismatch_marker}
  301. [:{mismatch_marker_alternate}]
  302. barbazquux
  303. """,
  304. """
  305. install_requires_with_extra
  306. {'cmd': ['egg_info']}
  307. install_requires=["barbazquux [test]"],
  308. [options]
  309. install_requires =
  310. barbazquux [test]
  311. barbazquux[test]
  312. """,
  313. """
  314. install_requires_with_extra_and_marker
  315. install_requires=["barbazquux [test]; {mismatch_marker}"],
  316. [options]
  317. install_requires =
  318. barbazquux [test]; {mismatch_marker}
  319. [:{mismatch_marker_alternate}]
  320. barbazquux[test]
  321. """,
  322. """
  323. setup_requires_with_markers
  324. setup_requires=["barbazquux;{mismatch_marker}"],
  325. [options]
  326. setup_requires =
  327. barbazquux; {mismatch_marker}
  328. """,
  329. """
  330. extras_require_with_extra
  331. {'cmd': ['egg_info']}
  332. extras_require={{"extra": ["barbazquux [test]"]}},
  333. [options.extras_require]
  334. extra = barbazquux [test]
  335. [extra]
  336. barbazquux[test]
  337. """,
  338. """
  339. extras_require_with_extra_and_marker_in_req
  340. extras_require={{"extra": ["barbazquux [test]; {mismatch_marker}"]}},
  341. [options.extras_require]
  342. extra =
  343. barbazquux [test]; {mismatch_marker}
  344. [extra]
  345. [extra:{mismatch_marker_alternate}]
  346. barbazquux[test]
  347. """,
  348. # FIXME: ConfigParser does not allow : in key names!
  349. """
  350. extras_require_with_marker
  351. extras_require={{":{mismatch_marker}": ["barbazquux"]}},
  352. @xfail
  353. [options.extras_require]
  354. :{mismatch_marker} = barbazquux
  355. [:{mismatch_marker}]
  356. barbazquux
  357. """,
  358. """
  359. extras_require_with_marker_in_req
  360. extras_require={{"extra": ["barbazquux; {mismatch_marker}"]}},
  361. [options.extras_require]
  362. extra =
  363. barbazquux; {mismatch_marker}
  364. [extra]
  365. [extra:{mismatch_marker_alternate}]
  366. barbazquux
  367. """,
  368. """
  369. extras_require_with_empty_section
  370. extras_require={{"empty": []}},
  371. [options.extras_require]
  372. empty =
  373. [empty]
  374. """,
  375. # Format arguments.
  376. invalid_marker=invalid_marker,
  377. mismatch_marker=mismatch_marker,
  378. mismatch_marker_alternate=mismatch_marker_alternate,
  379. )
  380. def test_requires(
  381. self,
  382. tmpdir_cwd,
  383. env,
  384. requires,
  385. use_setup_cfg,
  386. expected_requires,
  387. install_cmd_kwargs,
  388. ):
  389. self._setup_script_with_requires(requires, use_setup_cfg)
  390. self._run_egg_info_command(tmpdir_cwd, env, **install_cmd_kwargs)
  391. egg_info_dir = os.path.join('.', 'foo.egg-info')
  392. requires_txt = os.path.join(egg_info_dir, 'requires.txt')
  393. if os.path.exists(requires_txt):
  394. with open(requires_txt, encoding="utf-8") as fp:
  395. install_requires = fp.read()
  396. else:
  397. install_requires = ''
  398. assert install_requires.lstrip() == expected_requires
  399. assert glob.glob(os.path.join(env.paths['lib'], 'barbazquux*')) == []
  400. def test_install_requires_unordered_disallowed(self, tmpdir_cwd, env):
  401. """
  402. Packages that pass unordered install_requires sequences
  403. should be rejected as they produce non-deterministic
  404. builds. See #458.
  405. """
  406. req = 'install_requires={"fake-factory==0.5.2", "pytz"}'
  407. self._setup_script_with_requires(req)
  408. with pytest.raises(AssertionError):
  409. self._run_egg_info_command(tmpdir_cwd, env)
  410. def test_extras_require_with_invalid_marker(self, tmpdir_cwd, env):
  411. tmpl = 'extras_require={{":{marker}": ["barbazquux"]}},'
  412. req = tmpl.format(marker=self.invalid_marker)
  413. self._setup_script_with_requires(req)
  414. with pytest.raises(AssertionError):
  415. self._run_egg_info_command(tmpdir_cwd, env)
  416. assert glob.glob(os.path.join(env.paths['lib'], 'barbazquux*')) == []
  417. def test_extras_require_with_invalid_marker_in_req(self, tmpdir_cwd, env):
  418. tmpl = 'extras_require={{"extra": ["barbazquux; {marker}"]}},'
  419. req = tmpl.format(marker=self.invalid_marker)
  420. self._setup_script_with_requires(req)
  421. with pytest.raises(AssertionError):
  422. self._run_egg_info_command(tmpdir_cwd, env)
  423. assert glob.glob(os.path.join(env.paths['lib'], 'barbazquux*')) == []
  424. def test_provides_extra(self, tmpdir_cwd, env):
  425. self._setup_script_with_requires('extras_require={"foobar": ["barbazquux"]},')
  426. environ = os.environ.copy().update(
  427. HOME=env.paths['home'],
  428. )
  429. environment.run_setup_py(
  430. cmd=['egg_info'],
  431. pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
  432. data_stream=1,
  433. env=environ,
  434. )
  435. egg_info_dir = os.path.join('.', 'foo.egg-info')
  436. with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp:
  437. pkg_info_lines = fp.read().split('\n')
  438. assert 'Provides-Extra: foobar' in pkg_info_lines
  439. assert 'Metadata-Version: 2.4' in pkg_info_lines
  440. def test_doesnt_provides_extra(self, tmpdir_cwd, env):
  441. self._setup_script_with_requires(
  442. """install_requires=["spam ; python_version<'3.6'"]"""
  443. )
  444. environ = os.environ.copy().update(
  445. HOME=env.paths['home'],
  446. )
  447. environment.run_setup_py(
  448. cmd=['egg_info'],
  449. pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
  450. data_stream=1,
  451. env=environ,
  452. )
  453. egg_info_dir = os.path.join('.', 'foo.egg-info')
  454. with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp:
  455. pkg_info_text = fp.read()
  456. assert 'Provides-Extra:' not in pkg_info_text
  457. @pytest.mark.parametrize(
  458. ('files', 'license_in_sources'),
  459. [
  460. (
  461. {
  462. 'setup.cfg': DALS(
  463. """
  464. [metadata]
  465. license_file = LICENSE
  466. """
  467. ),
  468. 'LICENSE': "Test license",
  469. },
  470. True,
  471. ), # with license
  472. (
  473. {
  474. 'setup.cfg': DALS(
  475. """
  476. [metadata]
  477. license_file = INVALID_LICENSE
  478. """
  479. ),
  480. 'LICENSE': "Test license",
  481. },
  482. False,
  483. ), # with an invalid license
  484. (
  485. {
  486. 'setup.cfg': DALS(
  487. """
  488. """
  489. ),
  490. 'LICENSE': "Test license",
  491. },
  492. True,
  493. ), # no license_file attribute, LICENSE auto-included
  494. (
  495. {
  496. 'setup.cfg': DALS(
  497. """
  498. [metadata]
  499. license_file = LICENSE
  500. """
  501. ),
  502. 'MANIFEST.in': "exclude LICENSE",
  503. 'LICENSE': "Test license",
  504. },
  505. True,
  506. ), # manifest is overwritten by license_file
  507. pytest.param(
  508. {
  509. 'setup.cfg': DALS(
  510. """
  511. [metadata]
  512. license_file = LICEN[CS]E*
  513. """
  514. ),
  515. 'LICENSE': "Test license",
  516. },
  517. True,
  518. id="glob_pattern",
  519. ),
  520. ],
  521. )
  522. def test_setup_cfg_license_file(self, tmpdir_cwd, env, files, license_in_sources):
  523. self._create_project()
  524. path.build(files)
  525. environment.run_setup_py(
  526. cmd=['egg_info'],
  527. pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
  528. )
  529. egg_info_dir = os.path.join('.', 'foo.egg-info')
  530. sources_text = Path(egg_info_dir, "SOURCES.txt").read_text(encoding="utf-8")
  531. if license_in_sources:
  532. assert 'LICENSE' in sources_text
  533. else:
  534. assert 'LICENSE' not in sources_text
  535. # for invalid license test
  536. assert 'INVALID_LICENSE' not in sources_text
  537. @pytest.mark.parametrize(
  538. ('files', 'incl_licenses', 'excl_licenses'),
  539. [
  540. (
  541. {
  542. 'setup.cfg': DALS(
  543. """
  544. [metadata]
  545. license_files =
  546. LICENSE-ABC
  547. LICENSE-XYZ
  548. """
  549. ),
  550. 'LICENSE-ABC': "ABC license",
  551. 'LICENSE-XYZ': "XYZ license",
  552. },
  553. ['LICENSE-ABC', 'LICENSE-XYZ'],
  554. [],
  555. ), # with licenses
  556. (
  557. {
  558. 'setup.cfg': DALS(
  559. """
  560. [metadata]
  561. license_files = LICENSE-ABC, LICENSE-XYZ
  562. """
  563. ),
  564. 'LICENSE-ABC': "ABC license",
  565. 'LICENSE-XYZ': "XYZ license",
  566. },
  567. ['LICENSE-ABC', 'LICENSE-XYZ'],
  568. [],
  569. ), # with commas
  570. (
  571. {
  572. 'setup.cfg': DALS(
  573. """
  574. [metadata]
  575. license_files =
  576. LICENSE-ABC
  577. """
  578. ),
  579. 'LICENSE-ABC': "ABC license",
  580. 'LICENSE-XYZ': "XYZ license",
  581. },
  582. ['LICENSE-ABC'],
  583. ['LICENSE-XYZ'],
  584. ), # with one license
  585. (
  586. {
  587. 'setup.cfg': DALS(
  588. """
  589. [metadata]
  590. license_files =
  591. """
  592. ),
  593. 'LICENSE-ABC': "ABC license",
  594. 'LICENSE-XYZ': "XYZ license",
  595. },
  596. [],
  597. ['LICENSE-ABC', 'LICENSE-XYZ'],
  598. ), # empty
  599. (
  600. {
  601. 'setup.cfg': DALS(
  602. """
  603. [metadata]
  604. license_files = LICENSE-XYZ
  605. """
  606. ),
  607. 'LICENSE-ABC': "ABC license",
  608. 'LICENSE-XYZ': "XYZ license",
  609. },
  610. ['LICENSE-XYZ'],
  611. ['LICENSE-ABC'],
  612. ), # on same line
  613. (
  614. {
  615. 'setup.cfg': DALS(
  616. """
  617. [metadata]
  618. license_files =
  619. LICENSE-ABC
  620. INVALID_LICENSE
  621. """
  622. ),
  623. 'LICENSE-ABC': "Test license",
  624. },
  625. ['LICENSE-ABC'],
  626. ['INVALID_LICENSE'],
  627. ), # with an invalid license
  628. (
  629. {
  630. 'setup.cfg': DALS(
  631. """
  632. """
  633. ),
  634. 'LICENSE': "Test license",
  635. },
  636. ['LICENSE'],
  637. [],
  638. ), # no license_files attribute, LICENSE auto-included
  639. (
  640. {
  641. 'setup.cfg': DALS(
  642. """
  643. [metadata]
  644. license_files = LICENSE
  645. """
  646. ),
  647. 'MANIFEST.in': "exclude LICENSE",
  648. 'LICENSE': "Test license",
  649. },
  650. ['LICENSE'],
  651. [],
  652. ), # manifest is overwritten by license_files
  653. (
  654. {
  655. 'setup.cfg': DALS(
  656. """
  657. [metadata]
  658. license_files =
  659. LICENSE-ABC
  660. LICENSE-XYZ
  661. """
  662. ),
  663. 'MANIFEST.in': "exclude LICENSE-XYZ",
  664. 'LICENSE-ABC': "ABC license",
  665. 'LICENSE-XYZ': "XYZ license",
  666. # manifest is overwritten by license_files
  667. },
  668. ['LICENSE-ABC', 'LICENSE-XYZ'],
  669. [],
  670. ),
  671. pytest.param(
  672. {
  673. 'setup.cfg': "",
  674. 'LICENSE-ABC': "ABC license",
  675. 'COPYING-ABC': "ABC copying",
  676. 'NOTICE-ABC': "ABC notice",
  677. 'AUTHORS-ABC': "ABC authors",
  678. 'LICENCE-XYZ': "XYZ license",
  679. 'LICENSE': "License",
  680. 'INVALID-LICENSE': "Invalid license",
  681. },
  682. [
  683. 'LICENSE-ABC',
  684. 'COPYING-ABC',
  685. 'NOTICE-ABC',
  686. 'AUTHORS-ABC',
  687. 'LICENCE-XYZ',
  688. 'LICENSE',
  689. ],
  690. ['INVALID-LICENSE'],
  691. # ('LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*')
  692. id="default_glob_patterns",
  693. ),
  694. pytest.param(
  695. {
  696. 'setup.cfg': DALS(
  697. """
  698. [metadata]
  699. license_files =
  700. LICENSE*
  701. """
  702. ),
  703. 'LICENSE-ABC': "ABC license",
  704. 'NOTICE-XYZ': "XYZ notice",
  705. },
  706. ['LICENSE-ABC'],
  707. ['NOTICE-XYZ'],
  708. id="no_default_glob_patterns",
  709. ),
  710. pytest.param(
  711. {
  712. 'setup.cfg': DALS(
  713. """
  714. [metadata]
  715. license_files =
  716. LICENSE-ABC
  717. LICENSE*
  718. """
  719. ),
  720. 'LICENSE-ABC': "ABC license",
  721. },
  722. ['LICENSE-ABC'],
  723. [],
  724. id="files_only_added_once",
  725. ),
  726. pytest.param(
  727. {
  728. 'setup.cfg': DALS(
  729. """
  730. [metadata]
  731. license_files = **/LICENSE
  732. """
  733. ),
  734. 'LICENSE': "ABC license",
  735. 'LICENSE-OTHER': "Don't include",
  736. 'vendor': {'LICENSE': "Vendor license"},
  737. },
  738. ['LICENSE', 'vendor/LICENSE'],
  739. ['LICENSE-OTHER'],
  740. id="recursive_glob",
  741. ),
  742. ],
  743. )
  744. def test_setup_cfg_license_files(
  745. self, tmpdir_cwd, env, files, incl_licenses, excl_licenses
  746. ):
  747. self._create_project()
  748. path.build(files)
  749. environment.run_setup_py(
  750. cmd=['egg_info'],
  751. pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
  752. )
  753. egg_info_dir = os.path.join('.', 'foo.egg-info')
  754. sources_text = Path(egg_info_dir, "SOURCES.txt").read_text(encoding="utf-8")
  755. sources_lines = [line.strip() for line in sources_text.splitlines()]
  756. for lf in incl_licenses:
  757. assert sources_lines.count(lf) == 1
  758. for lf in excl_licenses:
  759. assert sources_lines.count(lf) == 0
  760. @pytest.mark.parametrize(
  761. ('files', 'incl_licenses', 'excl_licenses'),
  762. [
  763. (
  764. {
  765. 'setup.cfg': DALS(
  766. """
  767. [metadata]
  768. license_file =
  769. license_files =
  770. """
  771. ),
  772. 'LICENSE-ABC': "ABC license",
  773. 'LICENSE-XYZ': "XYZ license",
  774. },
  775. [],
  776. ['LICENSE-ABC', 'LICENSE-XYZ'],
  777. ), # both empty
  778. (
  779. {
  780. 'setup.cfg': DALS(
  781. """
  782. [metadata]
  783. license_file =
  784. LICENSE-ABC
  785. LICENSE-XYZ
  786. """
  787. ),
  788. 'LICENSE-ABC': "ABC license",
  789. 'LICENSE-XYZ': "XYZ license",
  790. # license_file is still singular
  791. },
  792. [],
  793. ['LICENSE-ABC', 'LICENSE-XYZ'],
  794. ),
  795. (
  796. {
  797. 'setup.cfg': DALS(
  798. """
  799. [metadata]
  800. license_file = LICENSE-ABC
  801. license_files =
  802. LICENSE-XYZ
  803. LICENSE-PQR
  804. """
  805. ),
  806. 'LICENSE-ABC': "ABC license",
  807. 'LICENSE-PQR': "PQR license",
  808. 'LICENSE-XYZ': "XYZ license",
  809. },
  810. ['LICENSE-ABC', 'LICENSE-PQR', 'LICENSE-XYZ'],
  811. [],
  812. ), # combined
  813. (
  814. {
  815. 'setup.cfg': DALS(
  816. """
  817. [metadata]
  818. license_file = LICENSE-ABC
  819. license_files =
  820. LICENSE-ABC
  821. LICENSE-XYZ
  822. LICENSE-PQR
  823. """
  824. ),
  825. 'LICENSE-ABC': "ABC license",
  826. 'LICENSE-PQR': "PQR license",
  827. 'LICENSE-XYZ': "XYZ license",
  828. # duplicate license
  829. },
  830. ['LICENSE-ABC', 'LICENSE-PQR', 'LICENSE-XYZ'],
  831. [],
  832. ),
  833. (
  834. {
  835. 'setup.cfg': DALS(
  836. """
  837. [metadata]
  838. license_file = LICENSE-ABC
  839. license_files =
  840. LICENSE-XYZ
  841. """
  842. ),
  843. 'LICENSE-ABC': "ABC license",
  844. 'LICENSE-PQR': "PQR license",
  845. 'LICENSE-XYZ': "XYZ license",
  846. # combined subset
  847. },
  848. ['LICENSE-ABC', 'LICENSE-XYZ'],
  849. ['LICENSE-PQR'],
  850. ),
  851. (
  852. {
  853. 'setup.cfg': DALS(
  854. """
  855. [metadata]
  856. license_file = LICENSE-ABC
  857. license_files =
  858. LICENSE-XYZ
  859. LICENSE-PQR
  860. """
  861. ),
  862. 'LICENSE-PQR': "Test license",
  863. # with invalid licenses
  864. },
  865. ['LICENSE-PQR'],
  866. ['LICENSE-ABC', 'LICENSE-XYZ'],
  867. ),
  868. (
  869. {
  870. 'setup.cfg': DALS(
  871. """
  872. [metadata]
  873. license_file = LICENSE-ABC
  874. license_files =
  875. LICENSE-PQR
  876. LICENSE-XYZ
  877. """
  878. ),
  879. 'MANIFEST.in': "exclude LICENSE-ABC\nexclude LICENSE-PQR",
  880. 'LICENSE-ABC': "ABC license",
  881. 'LICENSE-PQR': "PQR license",
  882. 'LICENSE-XYZ': "XYZ license",
  883. # manifest is overwritten
  884. },
  885. ['LICENSE-ABC', 'LICENSE-PQR', 'LICENSE-XYZ'],
  886. [],
  887. ),
  888. pytest.param(
  889. {
  890. 'setup.cfg': DALS(
  891. """
  892. [metadata]
  893. license_file = LICENSE*
  894. """
  895. ),
  896. 'LICENSE-ABC': "ABC license",
  897. 'NOTICE-XYZ': "XYZ notice",
  898. },
  899. ['LICENSE-ABC'],
  900. ['NOTICE-XYZ'],
  901. id="no_default_glob_patterns",
  902. ),
  903. pytest.param(
  904. {
  905. 'setup.cfg': DALS(
  906. """
  907. [metadata]
  908. license_file = LICENSE*
  909. license_files =
  910. NOTICE*
  911. """
  912. ),
  913. 'LICENSE-ABC': "ABC license",
  914. 'NOTICE-ABC': "ABC notice",
  915. 'AUTHORS-ABC': "ABC authors",
  916. },
  917. ['LICENSE-ABC', 'NOTICE-ABC'],
  918. ['AUTHORS-ABC'],
  919. id="combined_glob_patterrns",
  920. ),
  921. ],
  922. )
  923. def test_setup_cfg_license_file_license_files(
  924. self, tmpdir_cwd, env, files, incl_licenses, excl_licenses
  925. ):
  926. self._create_project()
  927. path.build(files)
  928. environment.run_setup_py(
  929. cmd=['egg_info'],
  930. pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
  931. )
  932. egg_info_dir = os.path.join('.', 'foo.egg-info')
  933. sources_text = Path(egg_info_dir, "SOURCES.txt").read_text(encoding="utf-8")
  934. sources_lines = [line.strip() for line in sources_text.splitlines()]
  935. for lf in incl_licenses:
  936. assert sources_lines.count(lf) == 1
  937. for lf in excl_licenses:
  938. assert sources_lines.count(lf) == 0
  939. def test_license_file_attr_pkg_info(self, tmpdir_cwd, env):
  940. """All matched license files should have a corresponding License-File."""
  941. self._create_project()
  942. path.build({
  943. "setup.cfg": DALS(
  944. """
  945. [metadata]
  946. license_files =
  947. NOTICE*
  948. LICENSE*
  949. **/LICENSE
  950. """
  951. ),
  952. "LICENSE-ABC": "ABC license",
  953. "LICENSE-XYZ": "XYZ license",
  954. "NOTICE": "included",
  955. "IGNORE": "not include",
  956. "vendor": {'LICENSE': "Vendor license"},
  957. })
  958. environment.run_setup_py(
  959. cmd=['egg_info'],
  960. pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
  961. )
  962. egg_info_dir = os.path.join('.', 'foo.egg-info')
  963. with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp:
  964. pkg_info_lines = fp.read().split('\n')
  965. license_file_lines = [
  966. line for line in pkg_info_lines if line.startswith('License-File:')
  967. ]
  968. # Only 'NOTICE', LICENSE-ABC', and 'LICENSE-XYZ' should have been matched
  969. # Also assert that order from license_files is keeped
  970. assert len(license_file_lines) == 4
  971. assert "License-File: NOTICE" == license_file_lines[0]
  972. assert "License-File: LICENSE-ABC" in license_file_lines[1:]
  973. assert "License-File: LICENSE-XYZ" in license_file_lines[1:]
  974. assert "License-File: vendor/LICENSE" in license_file_lines[3]
  975. def test_metadata_version(self, tmpdir_cwd, env):
  976. """Make sure latest metadata version is used by default."""
  977. self._setup_script_with_requires("")
  978. environment.run_setup_py(
  979. cmd=['egg_info'],
  980. pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
  981. data_stream=1,
  982. )
  983. egg_info_dir = os.path.join('.', 'foo.egg-info')
  984. with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp:
  985. pkg_info_lines = fp.read().split('\n')
  986. # Update metadata version if changed
  987. assert self._extract_mv_version(pkg_info_lines) == (2, 4)
  988. def test_long_description_content_type(self, tmpdir_cwd, env):
  989. # Test that specifying a `long_description_content_type` keyword arg to
  990. # the `setup` function results in writing a `Description-Content-Type`
  991. # line to the `PKG-INFO` file in the `<distribution>.egg-info`
  992. # directory.
  993. # `Description-Content-Type` is described at
  994. # https://github.com/pypa/python-packaging-user-guide/pull/258
  995. self._setup_script_with_requires(
  996. """long_description_content_type='text/markdown',"""
  997. )
  998. environ = os.environ.copy().update(
  999. HOME=env.paths['home'],
  1000. )
  1001. environment.run_setup_py(
  1002. cmd=['egg_info'],
  1003. pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
  1004. data_stream=1,
  1005. env=environ,
  1006. )
  1007. egg_info_dir = os.path.join('.', 'foo.egg-info')
  1008. with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp:
  1009. pkg_info_lines = fp.read().split('\n')
  1010. expected_line = 'Description-Content-Type: text/markdown'
  1011. assert expected_line in pkg_info_lines
  1012. assert 'Metadata-Version: 2.4' in pkg_info_lines
  1013. def test_long_description(self, tmpdir_cwd, env):
  1014. # Test that specifying `long_description` and `long_description_content_type`
  1015. # keyword args to the `setup` function results in writing
  1016. # the description in the message payload of the `PKG-INFO` file
  1017. # in the `<distribution>.egg-info` directory.
  1018. self._setup_script_with_requires(
  1019. "long_description='This is a long description\\nover multiple lines',"
  1020. "long_description_content_type='text/markdown',"
  1021. )
  1022. environment.run_setup_py(
  1023. cmd=['egg_info'],
  1024. pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
  1025. data_stream=1,
  1026. )
  1027. egg_info_dir = os.path.join('.', 'foo.egg-info')
  1028. with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp:
  1029. pkg_info_lines = fp.read().split('\n')
  1030. assert 'Metadata-Version: 2.4' in pkg_info_lines
  1031. assert '' == pkg_info_lines[-1] # last line should be empty
  1032. long_desc_lines = pkg_info_lines[pkg_info_lines.index('') :]
  1033. assert 'This is a long description' in long_desc_lines
  1034. assert 'over multiple lines' in long_desc_lines
  1035. def test_project_urls(self, tmpdir_cwd, env):
  1036. # Test that specifying a `project_urls` dict to the `setup`
  1037. # function results in writing multiple `Project-URL` lines to
  1038. # the `PKG-INFO` file in the `<distribution>.egg-info`
  1039. # directory.
  1040. # `Project-URL` is described at https://packaging.python.org
  1041. # /specifications/core-metadata/#project-url-multiple-use
  1042. self._setup_script_with_requires(
  1043. """project_urls={
  1044. 'Link One': 'https://example.com/one/',
  1045. 'Link Two': 'https://example.com/two/',
  1046. },"""
  1047. )
  1048. environ = os.environ.copy().update(
  1049. HOME=env.paths['home'],
  1050. )
  1051. environment.run_setup_py(
  1052. cmd=['egg_info'],
  1053. pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
  1054. data_stream=1,
  1055. env=environ,
  1056. )
  1057. egg_info_dir = os.path.join('.', 'foo.egg-info')
  1058. with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp:
  1059. pkg_info_lines = fp.read().split('\n')
  1060. expected_line = 'Project-URL: Link One, https://example.com/one/'
  1061. assert expected_line in pkg_info_lines
  1062. expected_line = 'Project-URL: Link Two, https://example.com/two/'
  1063. assert expected_line in pkg_info_lines
  1064. assert self._extract_mv_version(pkg_info_lines) >= (1, 2)
  1065. def test_license(self, tmpdir_cwd, env):
  1066. """Test single line license."""
  1067. self._setup_script_with_requires("license='MIT',")
  1068. environment.run_setup_py(
  1069. cmd=['egg_info'],
  1070. pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
  1071. data_stream=1,
  1072. )
  1073. egg_info_dir = os.path.join('.', 'foo.egg-info')
  1074. with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp:
  1075. pkg_info_lines = fp.read().split('\n')
  1076. assert 'License: MIT' in pkg_info_lines
  1077. def test_license_escape(self, tmpdir_cwd, env):
  1078. """Test license is escaped correctly if longer than one line."""
  1079. self._setup_script_with_requires(
  1080. "license='This is a long license text \\nover multiple lines',"
  1081. )
  1082. environment.run_setup_py(
  1083. cmd=['egg_info'],
  1084. pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
  1085. data_stream=1,
  1086. )
  1087. egg_info_dir = os.path.join('.', 'foo.egg-info')
  1088. with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp:
  1089. pkg_info_lines = fp.read().split('\n')
  1090. assert 'License: This is a long license text ' in pkg_info_lines
  1091. assert ' over multiple lines' in pkg_info_lines
  1092. assert 'text \n over multiple' in '\n'.join(pkg_info_lines)
  1093. def test_python_requires_egg_info(self, tmpdir_cwd, env):
  1094. self._setup_script_with_requires("""python_requires='>=2.7.12',""")
  1095. environ = os.environ.copy().update(
  1096. HOME=env.paths['home'],
  1097. )
  1098. environment.run_setup_py(
  1099. cmd=['egg_info'],
  1100. pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
  1101. data_stream=1,
  1102. env=environ,
  1103. )
  1104. egg_info_dir = os.path.join('.', 'foo.egg-info')
  1105. with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp:
  1106. pkg_info_lines = fp.read().split('\n')
  1107. assert 'Requires-Python: >=2.7.12' in pkg_info_lines
  1108. assert self._extract_mv_version(pkg_info_lines) >= (1, 2)
  1109. def test_manifest_maker_warning_suppression(self):
  1110. fixtures = [
  1111. "standard file not found: should have one of foo.py, bar.py",
  1112. "standard file 'setup.py' not found",
  1113. ]
  1114. for msg in fixtures:
  1115. assert manifest_maker._should_suppress_warning(msg)
  1116. def test_egg_info_includes_setup_py(self, tmpdir_cwd):
  1117. self._create_project()
  1118. dist = Distribution({"name": "foo", "version": "0.0.1"})
  1119. dist.script_name = "non_setup.py"
  1120. egg_info_instance = egg_info(dist)
  1121. egg_info_instance.finalize_options()
  1122. egg_info_instance.run()
  1123. assert 'setup.py' in egg_info_instance.filelist.files
  1124. with open(egg_info_instance.egg_info + "/SOURCES.txt", encoding="utf-8") as f:
  1125. sources = f.read().split('\n')
  1126. assert 'setup.py' in sources
  1127. def _run_egg_info_command(self, tmpdir_cwd, env, cmd=None, output=None):
  1128. environ = os.environ.copy().update(
  1129. HOME=env.paths['home'],
  1130. )
  1131. if cmd is None:
  1132. cmd = [
  1133. 'egg_info',
  1134. ]
  1135. code, data = environment.run_setup_py(
  1136. cmd=cmd,
  1137. pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
  1138. data_stream=1,
  1139. env=environ,
  1140. )
  1141. assert not code, data
  1142. if output:
  1143. assert output in data
  1144. def test_egg_info_tag_only_once(self, tmpdir_cwd, env):
  1145. self._create_project()
  1146. path.build({
  1147. 'setup.cfg': DALS(
  1148. """
  1149. [egg_info]
  1150. tag_build = dev
  1151. tag_date = 0
  1152. tag_svn_revision = 0
  1153. """
  1154. ),
  1155. })
  1156. self._run_egg_info_command(tmpdir_cwd, env)
  1157. egg_info_dir = os.path.join('.', 'foo.egg-info')
  1158. with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp:
  1159. pkg_info_lines = fp.read().split('\n')
  1160. assert 'Version: 0.0.0.dev0' in pkg_info_lines
  1161. class TestWriteEntries:
  1162. def test_invalid_entry_point(self, tmpdir_cwd, env):
  1163. dist = Distribution({"name": "foo", "version": "0.0.1"})
  1164. dist.entry_points = {"foo": "foo = invalid-identifier:foo"}
  1165. cmd = dist.get_command_obj("egg_info")
  1166. expected_msg = r"(Invalid object reference|Problems to parse)"
  1167. with pytest.raises((errors.OptionError, ValueError), match=expected_msg) as ex:
  1168. write_entries(cmd, "entry_points", "entry_points.txt")
  1169. assert "ensure entry-point follows the spec" in ex.value.args[0]
  1170. assert "invalid-identifier" in str(ex.value)
  1171. def test_valid_entry_point(self, tmpdir_cwd, env):
  1172. dist = Distribution({"name": "foo", "version": "0.0.1"})
  1173. dist.entry_points = {
  1174. "abc": "foo = bar:baz",
  1175. "def": ["faa = bor:boz"],
  1176. }
  1177. cmd = dist.get_command_obj("egg_info")
  1178. write_entries(cmd, "entry_points", "entry_points.txt")
  1179. content = Path("entry_points.txt").read_text(encoding="utf-8")
  1180. assert "[abc]\nfoo = bar:baz\n" in content
  1181. assert "[def]\nfaa = bor:boz\n" in content