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