test_build_meta.py 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959
  1. import contextlib
  2. import importlib
  3. import os
  4. import re
  5. import shutil
  6. import signal
  7. import sys
  8. import tarfile
  9. import warnings
  10. from concurrent import futures
  11. from pathlib import Path
  12. from typing import Any, Callable
  13. from zipfile import ZipFile
  14. import pytest
  15. from jaraco import path
  16. from packaging.requirements import Requirement
  17. from setuptools.warnings import SetuptoolsDeprecationWarning
  18. from .textwrap import DALS
  19. SETUP_SCRIPT_STUB = "__import__('setuptools').setup()"
  20. TIMEOUT = int(os.getenv("TIMEOUT_BACKEND_TEST", "180")) # in seconds
  21. IS_PYPY = '__pypy__' in sys.builtin_module_names
  22. pytestmark = pytest.mark.skipif(
  23. sys.platform == "win32" and IS_PYPY,
  24. reason="The combination of PyPy + Windows + pytest-xdist + ProcessPoolExecutor "
  25. "is flaky and problematic",
  26. )
  27. class BuildBackendBase:
  28. def __init__(self, cwd='.', env=None, backend_name='setuptools.build_meta') -> None:
  29. self.cwd = cwd
  30. self.env = env or {}
  31. self.backend_name = backend_name
  32. class BuildBackend(BuildBackendBase):
  33. """PEP 517 Build Backend"""
  34. def __init__(self, *args, **kwargs) -> None:
  35. super().__init__(*args, **kwargs)
  36. self.pool = futures.ProcessPoolExecutor(max_workers=1)
  37. def __getattr__(self, name: str) -> Callable[..., Any]:
  38. """Handles arbitrary function invocations on the build backend."""
  39. def method(*args, **kw):
  40. root = os.path.abspath(self.cwd)
  41. caller = BuildBackendCaller(root, self.env, self.backend_name)
  42. pid = None
  43. try:
  44. pid = self.pool.submit(os.getpid).result(TIMEOUT)
  45. return self.pool.submit(caller, name, *args, **kw).result(TIMEOUT)
  46. except futures.TimeoutError:
  47. self.pool.shutdown(wait=False) # doesn't stop already running processes
  48. self._kill(pid)
  49. pytest.xfail(f"Backend did not respond before timeout ({TIMEOUT} s)")
  50. except (futures.process.BrokenProcessPool, MemoryError, OSError):
  51. if IS_PYPY:
  52. pytest.xfail("PyPy frequently fails tests with ProcessPoolExector")
  53. raise
  54. return method
  55. def _kill(self, pid):
  56. if pid is None:
  57. return
  58. with contextlib.suppress(ProcessLookupError, OSError):
  59. os.kill(pid, signal.SIGTERM if os.name == "nt" else signal.SIGKILL)
  60. class BuildBackendCaller(BuildBackendBase):
  61. def __init__(self, *args, **kwargs) -> None:
  62. super().__init__(*args, **kwargs)
  63. (self.backend_name, _, self.backend_obj) = self.backend_name.partition(':')
  64. def __call__(self, name, *args, **kw) -> Any:
  65. """Handles arbitrary function invocations on the build backend."""
  66. os.chdir(self.cwd)
  67. os.environ.update(self.env)
  68. mod = importlib.import_module(self.backend_name)
  69. if self.backend_obj:
  70. backend = getattr(mod, self.backend_obj)
  71. else:
  72. backend = mod
  73. return getattr(backend, name)(*args, **kw)
  74. defns = [
  75. { # simple setup.py script
  76. 'setup.py': DALS(
  77. """
  78. __import__('setuptools').setup(
  79. name='foo',
  80. version='0.0.0',
  81. py_modules=['hello'],
  82. setup_requires=['six'],
  83. )
  84. """
  85. ),
  86. 'hello.py': DALS(
  87. """
  88. def run():
  89. print('hello')
  90. """
  91. ),
  92. },
  93. { # setup.py that relies on __name__
  94. 'setup.py': DALS(
  95. """
  96. assert __name__ == '__main__'
  97. __import__('setuptools').setup(
  98. name='foo',
  99. version='0.0.0',
  100. py_modules=['hello'],
  101. setup_requires=['six'],
  102. )
  103. """
  104. ),
  105. 'hello.py': DALS(
  106. """
  107. def run():
  108. print('hello')
  109. """
  110. ),
  111. },
  112. { # setup.py script that runs arbitrary code
  113. 'setup.py': DALS(
  114. """
  115. variable = True
  116. def function():
  117. return variable
  118. assert variable
  119. __import__('setuptools').setup(
  120. name='foo',
  121. version='0.0.0',
  122. py_modules=['hello'],
  123. setup_requires=['six'],
  124. )
  125. """
  126. ),
  127. 'hello.py': DALS(
  128. """
  129. def run():
  130. print('hello')
  131. """
  132. ),
  133. },
  134. { # setup.py script that constructs temp files to be included in the distribution
  135. 'setup.py': DALS(
  136. """
  137. # Some packages construct files on the fly, include them in the package,
  138. # and immediately remove them after `setup()` (e.g. pybind11==2.9.1).
  139. # Therefore, we cannot use `distutils.core.run_setup(..., stop_after=...)`
  140. # to obtain a distribution object first, and then run the distutils
  141. # commands later, because these files will be removed in the meantime.
  142. with open('world.py', 'w', encoding="utf-8") as f:
  143. f.write('x = 42')
  144. try:
  145. __import__('setuptools').setup(
  146. name='foo',
  147. version='0.0.0',
  148. py_modules=['world'],
  149. setup_requires=['six'],
  150. )
  151. finally:
  152. # Some packages will clean temporary files
  153. __import__('os').unlink('world.py')
  154. """
  155. ),
  156. },
  157. { # setup.cfg only
  158. 'setup.cfg': DALS(
  159. """
  160. [metadata]
  161. name = foo
  162. version = 0.0.0
  163. [options]
  164. py_modules=hello
  165. setup_requires=six
  166. """
  167. ),
  168. 'hello.py': DALS(
  169. """
  170. def run():
  171. print('hello')
  172. """
  173. ),
  174. },
  175. { # setup.cfg and setup.py
  176. 'setup.cfg': DALS(
  177. """
  178. [metadata]
  179. name = foo
  180. version = 0.0.0
  181. [options]
  182. py_modules=hello
  183. setup_requires=six
  184. """
  185. ),
  186. 'setup.py': "__import__('setuptools').setup()",
  187. 'hello.py': DALS(
  188. """
  189. def run():
  190. print('hello')
  191. """
  192. ),
  193. },
  194. ]
  195. class TestBuildMetaBackend:
  196. backend_name = 'setuptools.build_meta'
  197. def get_build_backend(self):
  198. return BuildBackend(backend_name=self.backend_name)
  199. @pytest.fixture(params=defns)
  200. def build_backend(self, tmpdir, request):
  201. path.build(request.param, prefix=str(tmpdir))
  202. with tmpdir.as_cwd():
  203. yield self.get_build_backend()
  204. def test_get_requires_for_build_wheel(self, build_backend):
  205. actual = build_backend.get_requires_for_build_wheel()
  206. expected = ['six']
  207. assert sorted(actual) == sorted(expected)
  208. def test_get_requires_for_build_sdist(self, build_backend):
  209. actual = build_backend.get_requires_for_build_sdist()
  210. expected = ['six']
  211. assert sorted(actual) == sorted(expected)
  212. def test_build_wheel(self, build_backend):
  213. dist_dir = os.path.abspath('pip-wheel')
  214. os.makedirs(dist_dir)
  215. wheel_name = build_backend.build_wheel(dist_dir)
  216. wheel_file = os.path.join(dist_dir, wheel_name)
  217. assert os.path.isfile(wheel_file)
  218. # Temporary files should be removed
  219. assert not os.path.isfile('world.py')
  220. with ZipFile(wheel_file) as zipfile:
  221. wheel_contents = set(zipfile.namelist())
  222. # Each one of the examples have a single module
  223. # that should be included in the distribution
  224. python_scripts = (f for f in wheel_contents if f.endswith('.py'))
  225. modules = [f for f in python_scripts if not f.endswith('setup.py')]
  226. assert len(modules) == 1
  227. @pytest.mark.parametrize('build_type', ('wheel', 'sdist'))
  228. def test_build_with_existing_file_present(self, build_type, tmpdir_cwd):
  229. # Building a sdist/wheel should still succeed if there's
  230. # already a sdist/wheel in the destination directory.
  231. files = {
  232. 'setup.py': "from setuptools import setup\nsetup()",
  233. 'VERSION': "0.0.1",
  234. 'setup.cfg': DALS(
  235. """
  236. [metadata]
  237. name = foo
  238. version = file: VERSION
  239. """
  240. ),
  241. 'pyproject.toml': DALS(
  242. """
  243. [build-system]
  244. requires = ["setuptools", "wheel"]
  245. build-backend = "setuptools.build_meta"
  246. """
  247. ),
  248. }
  249. path.build(files)
  250. dist_dir = os.path.abspath('preexisting-' + build_type)
  251. build_backend = self.get_build_backend()
  252. build_method = getattr(build_backend, 'build_' + build_type)
  253. # Build a first sdist/wheel.
  254. # Note: this also check the destination directory is
  255. # successfully created if it does not exist already.
  256. first_result = build_method(dist_dir)
  257. # Change version.
  258. with open("VERSION", "wt", encoding="utf-8") as version_file:
  259. version_file.write("0.0.2")
  260. # Build a *second* sdist/wheel.
  261. second_result = build_method(dist_dir)
  262. assert os.path.isfile(os.path.join(dist_dir, first_result))
  263. assert first_result != second_result
  264. # And if rebuilding the exact same sdist/wheel?
  265. open(os.path.join(dist_dir, second_result), 'wb').close()
  266. third_result = build_method(dist_dir)
  267. assert third_result == second_result
  268. assert os.path.getsize(os.path.join(dist_dir, third_result)) > 0
  269. @pytest.mark.parametrize("setup_script", [None, SETUP_SCRIPT_STUB])
  270. def test_build_with_pyproject_config(self, tmpdir, setup_script):
  271. files = {
  272. 'pyproject.toml': DALS(
  273. """
  274. [build-system]
  275. requires = ["setuptools", "wheel"]
  276. build-backend = "setuptools.build_meta"
  277. [project]
  278. name = "foo"
  279. license = {text = "MIT"}
  280. description = "This is a Python package"
  281. dynamic = ["version", "readme"]
  282. classifiers = [
  283. "Development Status :: 5 - Production/Stable",
  284. "Intended Audience :: Developers"
  285. ]
  286. urls = {Homepage = "http://github.com"}
  287. dependencies = [
  288. "appdirs",
  289. ]
  290. [project.optional-dependencies]
  291. all = [
  292. "tomli>=1",
  293. "pyscaffold>=4,<5",
  294. 'importlib; python_version == "2.6"',
  295. ]
  296. [project.scripts]
  297. foo = "foo.cli:main"
  298. [tool.setuptools]
  299. zip-safe = false
  300. package-dir = {"" = "src"}
  301. packages = {find = {where = ["src"]}}
  302. license-files = ["LICENSE*"]
  303. [tool.setuptools.dynamic]
  304. version = {attr = "foo.__version__"}
  305. readme = {file = "README.rst"}
  306. [tool.distutils.sdist]
  307. formats = "gztar"
  308. """
  309. ),
  310. "MANIFEST.in": DALS(
  311. """
  312. global-include *.py *.txt
  313. global-exclude *.py[cod]
  314. """
  315. ),
  316. "README.rst": "This is a ``README``",
  317. "LICENSE.txt": "---- placeholder MIT license ----",
  318. "src": {
  319. "foo": {
  320. "__init__.py": "__version__ = '0.1'",
  321. "__init__.pyi": "__version__: str",
  322. "cli.py": "def main(): print('hello world')",
  323. "data.txt": "def main(): print('hello world')",
  324. "py.typed": "",
  325. }
  326. },
  327. }
  328. if setup_script:
  329. files["setup.py"] = setup_script
  330. build_backend = self.get_build_backend()
  331. with tmpdir.as_cwd():
  332. path.build(files)
  333. msgs = [
  334. "'tool.setuptools.license-files' is deprecated in favor of 'project.license-files'",
  335. "`project.license` as a TOML table is deprecated",
  336. ]
  337. with warnings.catch_warnings():
  338. for msg in msgs:
  339. warnings.filterwarnings("ignore", msg, SetuptoolsDeprecationWarning)
  340. sdist_path = build_backend.build_sdist("temp")
  341. wheel_file = build_backend.build_wheel("temp")
  342. with tarfile.open(os.path.join(tmpdir, "temp", sdist_path)) as tar:
  343. sdist_contents = set(tar.getnames())
  344. with ZipFile(os.path.join(tmpdir, "temp", wheel_file)) as zipfile:
  345. wheel_contents = set(zipfile.namelist())
  346. metadata = str(zipfile.read("foo-0.1.dist-info/METADATA"), "utf-8")
  347. license = str(
  348. zipfile.read("foo-0.1.dist-info/licenses/LICENSE.txt"), "utf-8"
  349. )
  350. epoints = str(zipfile.read("foo-0.1.dist-info/entry_points.txt"), "utf-8")
  351. assert sdist_contents - {"foo-0.1/setup.py"} == {
  352. 'foo-0.1',
  353. 'foo-0.1/LICENSE.txt',
  354. 'foo-0.1/MANIFEST.in',
  355. 'foo-0.1/PKG-INFO',
  356. 'foo-0.1/README.rst',
  357. 'foo-0.1/pyproject.toml',
  358. 'foo-0.1/setup.cfg',
  359. 'foo-0.1/src',
  360. 'foo-0.1/src/foo',
  361. 'foo-0.1/src/foo/__init__.py',
  362. 'foo-0.1/src/foo/__init__.pyi',
  363. 'foo-0.1/src/foo/cli.py',
  364. 'foo-0.1/src/foo/data.txt',
  365. 'foo-0.1/src/foo/py.typed',
  366. 'foo-0.1/src/foo.egg-info',
  367. 'foo-0.1/src/foo.egg-info/PKG-INFO',
  368. 'foo-0.1/src/foo.egg-info/SOURCES.txt',
  369. 'foo-0.1/src/foo.egg-info/dependency_links.txt',
  370. 'foo-0.1/src/foo.egg-info/entry_points.txt',
  371. 'foo-0.1/src/foo.egg-info/requires.txt',
  372. 'foo-0.1/src/foo.egg-info/top_level.txt',
  373. 'foo-0.1/src/foo.egg-info/not-zip-safe',
  374. }
  375. assert wheel_contents == {
  376. "foo/__init__.py",
  377. "foo/__init__.pyi", # include type information by default
  378. "foo/cli.py",
  379. "foo/data.txt", # include_package_data defaults to True
  380. "foo/py.typed", # include type information by default
  381. "foo-0.1.dist-info/licenses/LICENSE.txt",
  382. "foo-0.1.dist-info/METADATA",
  383. "foo-0.1.dist-info/WHEEL",
  384. "foo-0.1.dist-info/entry_points.txt",
  385. "foo-0.1.dist-info/top_level.txt",
  386. "foo-0.1.dist-info/RECORD",
  387. }
  388. assert license == "---- placeholder MIT license ----"
  389. for line in (
  390. "Summary: This is a Python package",
  391. "License: MIT",
  392. "License-File: LICENSE.txt",
  393. "Classifier: Intended Audience :: Developers",
  394. "Requires-Dist: appdirs",
  395. "Requires-Dist: " + str(Requirement('tomli>=1 ; extra == "all"')),
  396. "Requires-Dist: "
  397. + str(Requirement('importlib; python_version=="2.6" and extra =="all"')),
  398. ):
  399. assert line in metadata, (line, metadata)
  400. assert metadata.strip().endswith("This is a ``README``")
  401. assert epoints.strip() == "[console_scripts]\nfoo = foo.cli:main"
  402. def test_static_metadata_in_pyproject_config(self, tmpdir):
  403. # Make sure static metadata in pyproject.toml is not overwritten by setup.py
  404. # as required by PEP 621
  405. files = {
  406. 'pyproject.toml': DALS(
  407. """
  408. [build-system]
  409. requires = ["setuptools", "wheel"]
  410. build-backend = "setuptools.build_meta"
  411. [project]
  412. name = "foo"
  413. description = "This is a Python package"
  414. version = "42"
  415. dependencies = ["six"]
  416. """
  417. ),
  418. 'hello.py': DALS(
  419. """
  420. def run():
  421. print('hello')
  422. """
  423. ),
  424. 'setup.py': DALS(
  425. """
  426. __import__('setuptools').setup(
  427. name='bar',
  428. version='13',
  429. )
  430. """
  431. ),
  432. }
  433. build_backend = self.get_build_backend()
  434. with tmpdir.as_cwd():
  435. path.build(files)
  436. sdist_path = build_backend.build_sdist("temp")
  437. wheel_file = build_backend.build_wheel("temp")
  438. assert (tmpdir / "temp/foo-42.tar.gz").exists()
  439. assert (tmpdir / "temp/foo-42-py3-none-any.whl").exists()
  440. assert not (tmpdir / "temp/bar-13.tar.gz").exists()
  441. assert not (tmpdir / "temp/bar-42.tar.gz").exists()
  442. assert not (tmpdir / "temp/foo-13.tar.gz").exists()
  443. assert not (tmpdir / "temp/bar-13-py3-none-any.whl").exists()
  444. assert not (tmpdir / "temp/bar-42-py3-none-any.whl").exists()
  445. assert not (tmpdir / "temp/foo-13-py3-none-any.whl").exists()
  446. with tarfile.open(os.path.join(tmpdir, "temp", sdist_path)) as tar:
  447. pkg_info = str(tar.extractfile('foo-42/PKG-INFO').read(), "utf-8")
  448. members = tar.getnames()
  449. assert "bar-13/PKG-INFO" not in members
  450. with ZipFile(os.path.join(tmpdir, "temp", wheel_file)) as zipfile:
  451. metadata = str(zipfile.read("foo-42.dist-info/METADATA"), "utf-8")
  452. members = zipfile.namelist()
  453. assert "bar-13.dist-info/METADATA" not in members
  454. for file in pkg_info, metadata:
  455. for line in ("Name: foo", "Version: 42"):
  456. assert line in file
  457. for line in ("Name: bar", "Version: 13"):
  458. assert line not in file
  459. def test_build_sdist(self, build_backend):
  460. dist_dir = os.path.abspath('pip-sdist')
  461. os.makedirs(dist_dir)
  462. sdist_name = build_backend.build_sdist(dist_dir)
  463. assert os.path.isfile(os.path.join(dist_dir, sdist_name))
  464. def test_prepare_metadata_for_build_wheel(self, build_backend):
  465. dist_dir = os.path.abspath('pip-dist-info')
  466. os.makedirs(dist_dir)
  467. dist_info = build_backend.prepare_metadata_for_build_wheel(dist_dir)
  468. assert os.path.isfile(os.path.join(dist_dir, dist_info, 'METADATA'))
  469. def test_prepare_metadata_inplace(self, build_backend):
  470. """
  471. Some users might pass metadata_directory pre-populated with `.tox` or `.venv`.
  472. See issue #3523.
  473. """
  474. for pre_existing in [
  475. ".tox/python/lib/python3.10/site-packages/attrs-22.1.0.dist-info",
  476. ".tox/python/lib/python3.10/site-packages/autocommand-2.2.1.dist-info",
  477. ".nox/python/lib/python3.10/site-packages/build-0.8.0.dist-info",
  478. ".venv/python3.10/site-packages/click-8.1.3.dist-info",
  479. "venv/python3.10/site-packages/distlib-0.3.5.dist-info",
  480. "env/python3.10/site-packages/docutils-0.19.dist-info",
  481. ]:
  482. os.makedirs(pre_existing, exist_ok=True)
  483. dist_info = build_backend.prepare_metadata_for_build_wheel(".")
  484. assert os.path.isfile(os.path.join(dist_info, 'METADATA'))
  485. def test_build_sdist_explicit_dist(self, build_backend):
  486. # explicitly specifying the dist folder should work
  487. # the folder sdist_directory and the ``--dist-dir`` can be the same
  488. dist_dir = os.path.abspath('dist')
  489. sdist_name = build_backend.build_sdist(dist_dir)
  490. assert os.path.isfile(os.path.join(dist_dir, sdist_name))
  491. def test_build_sdist_version_change(self, build_backend):
  492. sdist_into_directory = os.path.abspath("out_sdist")
  493. os.makedirs(sdist_into_directory)
  494. sdist_name = build_backend.build_sdist(sdist_into_directory)
  495. assert os.path.isfile(os.path.join(sdist_into_directory, sdist_name))
  496. # if the setup.py changes subsequent call of the build meta
  497. # should still succeed, given the
  498. # sdist_directory the frontend specifies is empty
  499. setup_loc = os.path.abspath("setup.py")
  500. if not os.path.exists(setup_loc):
  501. setup_loc = os.path.abspath("setup.cfg")
  502. with open(setup_loc, 'rt', encoding="utf-8") as file_handler:
  503. content = file_handler.read()
  504. with open(setup_loc, 'wt', encoding="utf-8") as file_handler:
  505. file_handler.write(content.replace("version='0.0.0'", "version='0.0.1'"))
  506. shutil.rmtree(sdist_into_directory)
  507. os.makedirs(sdist_into_directory)
  508. sdist_name = build_backend.build_sdist("out_sdist")
  509. assert os.path.isfile(os.path.join(os.path.abspath("out_sdist"), sdist_name))
  510. def test_build_sdist_pyproject_toml_exists(self, tmpdir_cwd):
  511. files = {
  512. 'setup.py': DALS(
  513. """
  514. __import__('setuptools').setup(
  515. name='foo',
  516. version='0.0.0',
  517. py_modules=['hello']
  518. )"""
  519. ),
  520. 'hello.py': '',
  521. 'pyproject.toml': DALS(
  522. """
  523. [build-system]
  524. requires = ["setuptools", "wheel"]
  525. build-backend = "setuptools.build_meta"
  526. """
  527. ),
  528. }
  529. path.build(files)
  530. build_backend = self.get_build_backend()
  531. targz_path = build_backend.build_sdist("temp")
  532. with tarfile.open(os.path.join("temp", targz_path)) as tar:
  533. assert any('pyproject.toml' in name for name in tar.getnames())
  534. def test_build_sdist_setup_py_exists(self, tmpdir_cwd):
  535. # If build_sdist is called from a script other than setup.py,
  536. # ensure setup.py is included
  537. path.build(defns[0])
  538. build_backend = self.get_build_backend()
  539. targz_path = build_backend.build_sdist("temp")
  540. with tarfile.open(os.path.join("temp", targz_path)) as tar:
  541. assert any('setup.py' in name for name in tar.getnames())
  542. def test_build_sdist_setup_py_manifest_excluded(self, tmpdir_cwd):
  543. # Ensure that MANIFEST.in can exclude setup.py
  544. files = {
  545. 'setup.py': DALS(
  546. """
  547. __import__('setuptools').setup(
  548. name='foo',
  549. version='0.0.0',
  550. py_modules=['hello']
  551. )"""
  552. ),
  553. 'hello.py': '',
  554. 'MANIFEST.in': DALS(
  555. """
  556. exclude setup.py
  557. """
  558. ),
  559. }
  560. path.build(files)
  561. build_backend = self.get_build_backend()
  562. targz_path = build_backend.build_sdist("temp")
  563. with tarfile.open(os.path.join("temp", targz_path)) as tar:
  564. assert not any('setup.py' in name for name in tar.getnames())
  565. def test_build_sdist_builds_targz_even_if_zip_indicated(self, tmpdir_cwd):
  566. files = {
  567. 'setup.py': DALS(
  568. """
  569. __import__('setuptools').setup(
  570. name='foo',
  571. version='0.0.0',
  572. py_modules=['hello']
  573. )"""
  574. ),
  575. 'hello.py': '',
  576. 'setup.cfg': DALS(
  577. """
  578. [sdist]
  579. formats=zip
  580. """
  581. ),
  582. }
  583. path.build(files)
  584. build_backend = self.get_build_backend()
  585. build_backend.build_sdist("temp")
  586. _relative_path_import_files = {
  587. 'setup.py': DALS(
  588. """
  589. __import__('setuptools').setup(
  590. name='foo',
  591. version=__import__('hello').__version__,
  592. py_modules=['hello']
  593. )"""
  594. ),
  595. 'hello.py': '__version__ = "0.0.0"',
  596. 'setup.cfg': DALS(
  597. """
  598. [sdist]
  599. formats=zip
  600. """
  601. ),
  602. }
  603. def test_build_sdist_relative_path_import(self, tmpdir_cwd):
  604. path.build(self._relative_path_import_files)
  605. build_backend = self.get_build_backend()
  606. with pytest.raises(ImportError, match="^No module named 'hello'$"):
  607. build_backend.build_sdist("temp")
  608. _simple_pyproject_example = {
  609. "pyproject.toml": DALS(
  610. """
  611. [project]
  612. name = "proj"
  613. version = "42"
  614. """
  615. ),
  616. "src": {"proj": {"__init__.py": ""}},
  617. }
  618. def _assert_link_tree(self, parent_dir):
  619. """All files in the directory should be either links or hard links"""
  620. files = list(Path(parent_dir).glob("**/*"))
  621. assert files # Should not be empty
  622. for file in files:
  623. assert file.is_symlink() or os.stat(file).st_nlink > 0
  624. def test_editable_without_config_settings(self, tmpdir_cwd):
  625. """
  626. Sanity check to ensure tests with --mode=strict are different from the ones
  627. without --mode.
  628. --mode=strict should create a local directory with a package tree.
  629. The directory should not get created otherwise.
  630. """
  631. path.build(self._simple_pyproject_example)
  632. build_backend = self.get_build_backend()
  633. assert not Path("build").exists()
  634. build_backend.build_editable("temp")
  635. assert not Path("build").exists()
  636. def test_build_wheel_inplace(self, tmpdir_cwd):
  637. config_settings = {"--build-option": ["build_ext", "--inplace"]}
  638. path.build(self._simple_pyproject_example)
  639. build_backend = self.get_build_backend()
  640. assert not Path("build").exists()
  641. Path("build").mkdir()
  642. build_backend.prepare_metadata_for_build_wheel("build", config_settings)
  643. build_backend.build_wheel("build", config_settings)
  644. assert Path("build/proj-42-py3-none-any.whl").exists()
  645. @pytest.mark.parametrize("config_settings", [{"editable-mode": "strict"}])
  646. def test_editable_with_config_settings(self, tmpdir_cwd, config_settings):
  647. path.build({**self._simple_pyproject_example, '_meta': {}})
  648. assert not Path("build").exists()
  649. build_backend = self.get_build_backend()
  650. build_backend.prepare_metadata_for_build_editable("_meta", config_settings)
  651. build_backend.build_editable("temp", config_settings, "_meta")
  652. self._assert_link_tree(next(Path("build").glob("__editable__.*")))
  653. @pytest.mark.parametrize(
  654. ("setup_literal", "requirements"),
  655. [
  656. ("'foo'", ['foo']),
  657. ("['foo']", ['foo']),
  658. (r"'foo\n'", ['foo']),
  659. (r"'foo\n\n'", ['foo']),
  660. ("['foo', 'bar']", ['foo', 'bar']),
  661. (r"'# Has a comment line\nfoo'", ['foo']),
  662. (r"'foo # Has an inline comment'", ['foo']),
  663. (r"'foo \\\n >=3.0'", ['foo>=3.0']),
  664. (r"'foo\nbar'", ['foo', 'bar']),
  665. (r"'foo\nbar\n'", ['foo', 'bar']),
  666. (r"['foo\n', 'bar\n']", ['foo', 'bar']),
  667. ],
  668. )
  669. @pytest.mark.parametrize('use_wheel', [True, False])
  670. def test_setup_requires(self, setup_literal, requirements, use_wheel, tmpdir_cwd):
  671. files = {
  672. 'setup.py': DALS(
  673. """
  674. from setuptools import setup
  675. setup(
  676. name="qux",
  677. version="0.0.0",
  678. py_modules=["hello"],
  679. setup_requires={setup_literal},
  680. )
  681. """
  682. ).format(setup_literal=setup_literal),
  683. 'hello.py': DALS(
  684. """
  685. def run():
  686. print('hello')
  687. """
  688. ),
  689. }
  690. path.build(files)
  691. build_backend = self.get_build_backend()
  692. if use_wheel:
  693. get_requires = build_backend.get_requires_for_build_wheel
  694. else:
  695. get_requires = build_backend.get_requires_for_build_sdist
  696. # Ensure that the build requirements are properly parsed
  697. expected = sorted(requirements)
  698. actual = get_requires()
  699. assert expected == sorted(actual)
  700. def test_setup_requires_with_auto_discovery(self, tmpdir_cwd):
  701. # Make sure patches introduced to retrieve setup_requires don't accidentally
  702. # activate auto-discovery and cause problems due to the incomplete set of
  703. # attributes passed to MinimalDistribution
  704. files = {
  705. 'pyproject.toml': DALS(
  706. """
  707. [project]
  708. name = "proj"
  709. version = "42"
  710. """
  711. ),
  712. "setup.py": DALS(
  713. """
  714. __import__('setuptools').setup(
  715. setup_requires=["foo"],
  716. py_modules = ["hello", "world"]
  717. )
  718. """
  719. ),
  720. 'hello.py': "'hello'",
  721. 'world.py': "'world'",
  722. }
  723. path.build(files)
  724. build_backend = self.get_build_backend()
  725. setup_requires = build_backend.get_requires_for_build_wheel()
  726. assert setup_requires == ["foo"]
  727. def test_dont_install_setup_requires(self, tmpdir_cwd):
  728. files = {
  729. 'setup.py': DALS(
  730. """
  731. from setuptools import setup
  732. setup(
  733. name="qux",
  734. version="0.0.0",
  735. py_modules=["hello"],
  736. setup_requires=["does-not-exist >99"],
  737. )
  738. """
  739. ),
  740. 'hello.py': DALS(
  741. """
  742. def run():
  743. print('hello')
  744. """
  745. ),
  746. }
  747. path.build(files)
  748. build_backend = self.get_build_backend()
  749. dist_dir = os.path.abspath('pip-dist-info')
  750. os.makedirs(dist_dir)
  751. # does-not-exist can't be satisfied, so if it attempts to install
  752. # setup_requires, it will fail.
  753. build_backend.prepare_metadata_for_build_wheel(dist_dir)
  754. _sys_argv_0_passthrough = {
  755. 'setup.py': DALS(
  756. """
  757. import os
  758. import sys
  759. __import__('setuptools').setup(
  760. name='foo',
  761. version='0.0.0',
  762. )
  763. sys_argv = os.path.abspath(sys.argv[0])
  764. file_path = os.path.abspath('setup.py')
  765. assert sys_argv == file_path
  766. """
  767. )
  768. }
  769. def test_sys_argv_passthrough(self, tmpdir_cwd):
  770. path.build(self._sys_argv_0_passthrough)
  771. build_backend = self.get_build_backend()
  772. with pytest.raises(AssertionError):
  773. build_backend.build_sdist("temp")
  774. _setup_py_file_abspath = {
  775. 'setup.py': DALS(
  776. """
  777. import os
  778. assert os.path.isabs(__file__)
  779. __import__('setuptools').setup(
  780. name='foo',
  781. version='0.0.0',
  782. py_modules=['hello'],
  783. setup_requires=['six'],
  784. )
  785. """
  786. )
  787. }
  788. def test_setup_py_file_abspath(self, tmpdir_cwd):
  789. path.build(self._setup_py_file_abspath)
  790. build_backend = self.get_build_backend()
  791. build_backend.build_sdist("temp")
  792. @pytest.mark.parametrize('build_hook', ('build_sdist', 'build_wheel'))
  793. def test_build_with_empty_setuppy(self, build_backend, build_hook):
  794. files = {'setup.py': ''}
  795. path.build(files)
  796. msg = re.escape('No distribution was found.')
  797. with pytest.raises(ValueError, match=msg):
  798. getattr(build_backend, build_hook)("temp")
  799. class TestBuildMetaLegacyBackend(TestBuildMetaBackend):
  800. backend_name = 'setuptools.build_meta:__legacy__'
  801. # build_meta_legacy-specific tests
  802. def test_build_sdist_relative_path_import(self, tmpdir_cwd):
  803. # This must fail in build_meta, but must pass in build_meta_legacy
  804. path.build(self._relative_path_import_files)
  805. build_backend = self.get_build_backend()
  806. build_backend.build_sdist("temp")
  807. def test_sys_argv_passthrough(self, tmpdir_cwd):
  808. path.build(self._sys_argv_0_passthrough)
  809. build_backend = self.get_build_backend()
  810. build_backend.build_sdist("temp")
  811. @pytest.mark.filterwarnings("ignore::setuptools.SetuptoolsDeprecationWarning")
  812. def test_sys_exit_0_in_setuppy(monkeypatch, tmp_path):
  813. """Setuptools should be resilient to setup.py with ``sys.exit(0)`` (#3973)."""
  814. monkeypatch.chdir(tmp_path)
  815. setuppy = """
  816. import sys, setuptools
  817. setuptools.setup(name='foo', version='0.0.0')
  818. sys.exit(0)
  819. """
  820. (tmp_path / "setup.py").write_text(DALS(setuppy), encoding="utf-8")
  821. backend = BuildBackend(backend_name="setuptools.build_meta")
  822. assert backend.get_requires_for_build_wheel() == []
  823. def test_system_exit_in_setuppy(monkeypatch, tmp_path):
  824. monkeypatch.chdir(tmp_path)
  825. setuppy = "import sys; sys.exit('some error')"
  826. (tmp_path / "setup.py").write_text(setuppy, encoding="utf-8")
  827. with pytest.raises(SystemExit, match="some error"):
  828. backend = BuildBackend(backend_name="setuptools.build_meta")
  829. backend.get_requires_for_build_wheel()