test_config_discovery.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647
  1. import os
  2. import sys
  3. from configparser import ConfigParser
  4. from itertools import product
  5. from typing import cast
  6. import jaraco.path
  7. import pytest
  8. from path import Path
  9. import setuptools # noqa: F401 # force distutils.core to be patched
  10. from setuptools.command.sdist import sdist
  11. from setuptools.discovery import find_package_path, find_parent_package
  12. from setuptools.dist import Distribution
  13. from setuptools.errors import PackageDiscoveryError
  14. from .contexts import quiet
  15. from .integration.helpers import get_sdist_members, get_wheel_members, run
  16. from .textwrap import DALS
  17. import distutils.core
  18. class TestFindParentPackage:
  19. def test_single_package(self, tmp_path):
  20. # find_parent_package should find a non-namespace parent package
  21. (tmp_path / "src/namespace/pkg/nested").mkdir(exist_ok=True, parents=True)
  22. (tmp_path / "src/namespace/pkg/nested/__init__.py").touch()
  23. (tmp_path / "src/namespace/pkg/__init__.py").touch()
  24. packages = ["namespace", "namespace.pkg", "namespace.pkg.nested"]
  25. assert find_parent_package(packages, {"": "src"}, tmp_path) == "namespace.pkg"
  26. def test_multiple_toplevel(self, tmp_path):
  27. # find_parent_package should return null if the given list of packages does not
  28. # have a single parent package
  29. multiple = ["pkg", "pkg1", "pkg2"]
  30. for name in multiple:
  31. (tmp_path / f"src/{name}").mkdir(exist_ok=True, parents=True)
  32. (tmp_path / f"src/{name}/__init__.py").touch()
  33. assert find_parent_package(multiple, {"": "src"}, tmp_path) is None
  34. class TestDiscoverPackagesAndPyModules:
  35. """Make sure discovered values for ``packages`` and ``py_modules`` work
  36. similarly to explicit configuration for the simple scenarios.
  37. """
  38. OPTIONS = {
  39. # Different options according to the circumstance being tested
  40. "explicit-src": {"package_dir": {"": "src"}, "packages": ["pkg"]},
  41. "variation-lib": {
  42. "package_dir": {"": "lib"}, # variation of the source-layout
  43. },
  44. "explicit-flat": {"packages": ["pkg"]},
  45. "explicit-single_module": {"py_modules": ["pkg"]},
  46. "explicit-namespace": {"packages": ["ns", "ns.pkg"]},
  47. "automatic-src": {},
  48. "automatic-flat": {},
  49. "automatic-single_module": {},
  50. "automatic-namespace": {},
  51. }
  52. FILES = {
  53. "src": ["src/pkg/__init__.py", "src/pkg/main.py"],
  54. "lib": ["lib/pkg/__init__.py", "lib/pkg/main.py"],
  55. "flat": ["pkg/__init__.py", "pkg/main.py"],
  56. "single_module": ["pkg.py"],
  57. "namespace": ["ns/pkg/__init__.py"],
  58. }
  59. def _get_info(self, circumstance):
  60. _, _, layout = circumstance.partition("-")
  61. files = self.FILES[layout]
  62. options = self.OPTIONS[circumstance]
  63. return files, options
  64. @pytest.mark.parametrize("circumstance", OPTIONS.keys())
  65. def test_sdist_filelist(self, tmp_path, circumstance):
  66. files, options = self._get_info(circumstance)
  67. _populate_project_dir(tmp_path, files, options)
  68. _, cmd = _run_sdist_programatically(tmp_path, options)
  69. manifest = [f.replace(os.sep, "/") for f in cmd.filelist.files]
  70. for file in files:
  71. assert any(f.endswith(file) for f in manifest)
  72. @pytest.mark.parametrize("circumstance", OPTIONS.keys())
  73. def test_project(self, tmp_path, circumstance):
  74. files, options = self._get_info(circumstance)
  75. _populate_project_dir(tmp_path, files, options)
  76. # Simulate a pre-existing `build` directory
  77. (tmp_path / "build").mkdir()
  78. (tmp_path / "build/lib").mkdir()
  79. (tmp_path / "build/bdist.linux-x86_64").mkdir()
  80. (tmp_path / "build/bdist.linux-x86_64/file.py").touch()
  81. (tmp_path / "build/lib/__init__.py").touch()
  82. (tmp_path / "build/lib/file.py").touch()
  83. (tmp_path / "dist").mkdir()
  84. (tmp_path / "dist/file.py").touch()
  85. _run_build(tmp_path)
  86. sdist_files = get_sdist_members(next(tmp_path.glob("dist/*.tar.gz")))
  87. print("~~~~~ sdist_members ~~~~~")
  88. print('\n'.join(sdist_files))
  89. assert sdist_files >= set(files)
  90. wheel_files = get_wheel_members(next(tmp_path.glob("dist/*.whl")))
  91. print("~~~~~ wheel_members ~~~~~")
  92. print('\n'.join(wheel_files))
  93. orig_files = {f.replace("src/", "").replace("lib/", "") for f in files}
  94. assert wheel_files >= orig_files
  95. # Make sure build files are not included by mistake
  96. for file in wheel_files:
  97. assert "build" not in files
  98. assert "dist" not in files
  99. PURPOSEFULLY_EMPY = {
  100. "setup.cfg": DALS(
  101. """
  102. [metadata]
  103. name = myproj
  104. version = 0.0.0
  105. [options]
  106. {param} =
  107. """
  108. ),
  109. "setup.py": DALS(
  110. """
  111. __import__('setuptools').setup(
  112. name="myproj",
  113. version="0.0.0",
  114. {param}=[]
  115. )
  116. """
  117. ),
  118. "pyproject.toml": DALS(
  119. """
  120. [build-system]
  121. requires = []
  122. build-backend = 'setuptools.build_meta'
  123. [project]
  124. name = "myproj"
  125. version = "0.0.0"
  126. [tool.setuptools]
  127. {param} = []
  128. """
  129. ),
  130. "template-pyproject.toml": DALS(
  131. """
  132. [build-system]
  133. requires = []
  134. build-backend = 'setuptools.build_meta'
  135. """
  136. ),
  137. }
  138. @pytest.mark.parametrize(
  139. ("config_file", "param", "circumstance"),
  140. product(
  141. ["setup.cfg", "setup.py", "pyproject.toml"],
  142. ["packages", "py_modules"],
  143. FILES.keys(),
  144. ),
  145. )
  146. def test_purposefully_empty(self, tmp_path, config_file, param, circumstance):
  147. files = self.FILES[circumstance] + ["mod.py", "other.py", "src/pkg/__init__.py"]
  148. _populate_project_dir(tmp_path, files, {})
  149. if config_file == "pyproject.toml":
  150. template_param = param.replace("_", "-")
  151. else:
  152. # Make sure build works with or without setup.cfg
  153. pyproject = self.PURPOSEFULLY_EMPY["template-pyproject.toml"]
  154. (tmp_path / "pyproject.toml").write_text(pyproject, encoding="utf-8")
  155. template_param = param
  156. config = self.PURPOSEFULLY_EMPY[config_file].format(param=template_param)
  157. (tmp_path / config_file).write_text(config, encoding="utf-8")
  158. dist = _get_dist(tmp_path, {})
  159. # When either parameter package or py_modules is an empty list,
  160. # then there should be no discovery
  161. assert getattr(dist, param) == []
  162. other = {"py_modules": "packages", "packages": "py_modules"}[param]
  163. assert getattr(dist, other) is None
  164. @pytest.mark.parametrize(
  165. ("extra_files", "pkgs"),
  166. [
  167. (["venv/bin/simulate_venv"], {"pkg"}),
  168. (["pkg-stubs/__init__.pyi"], {"pkg", "pkg-stubs"}),
  169. (["other-stubs/__init__.pyi"], {"pkg", "other-stubs"}),
  170. (
  171. # Type stubs can also be namespaced
  172. ["namespace-stubs/pkg/__init__.pyi"],
  173. {"pkg", "namespace-stubs", "namespace-stubs.pkg"},
  174. ),
  175. (
  176. # Just the top-level package can have `-stubs`, ignore nested ones
  177. ["namespace-stubs/pkg-stubs/__init__.pyi"],
  178. {"pkg", "namespace-stubs"},
  179. ),
  180. (["_hidden/file.py"], {"pkg"}),
  181. (["news/finalize.py"], {"pkg"}),
  182. ],
  183. )
  184. def test_flat_layout_with_extra_files(self, tmp_path, extra_files, pkgs):
  185. files = self.FILES["flat"] + extra_files
  186. _populate_project_dir(tmp_path, files, {})
  187. dist = _get_dist(tmp_path, {})
  188. assert set(dist.packages) == pkgs
  189. @pytest.mark.parametrize(
  190. "extra_files",
  191. [
  192. ["other/__init__.py"],
  193. ["other/finalize.py"],
  194. ],
  195. )
  196. def test_flat_layout_with_dangerous_extra_files(self, tmp_path, extra_files):
  197. files = self.FILES["flat"] + extra_files
  198. _populate_project_dir(tmp_path, files, {})
  199. with pytest.raises(PackageDiscoveryError, match="multiple (packages|modules)"):
  200. _get_dist(tmp_path, {})
  201. def test_flat_layout_with_single_module(self, tmp_path):
  202. files = self.FILES["single_module"] + ["invalid-module-name.py"]
  203. _populate_project_dir(tmp_path, files, {})
  204. dist = _get_dist(tmp_path, {})
  205. assert set(dist.py_modules) == {"pkg"}
  206. def test_flat_layout_with_multiple_modules(self, tmp_path):
  207. files = self.FILES["single_module"] + ["valid_module_name.py"]
  208. _populate_project_dir(tmp_path, files, {})
  209. with pytest.raises(PackageDiscoveryError, match="multiple (packages|modules)"):
  210. _get_dist(tmp_path, {})
  211. def test_py_modules_when_wheel_dir_is_cwd(self, tmp_path):
  212. """Regression for issue 3692"""
  213. from setuptools import build_meta
  214. pyproject = '[project]\nname = "test"\nversion = "1"'
  215. (tmp_path / "pyproject.toml").write_text(DALS(pyproject), encoding="utf-8")
  216. (tmp_path / "foo.py").touch()
  217. with jaraco.path.DirectoryStack().context(tmp_path):
  218. build_meta.build_wheel(".")
  219. # Ensure py_modules are found
  220. wheel_files = get_wheel_members(next(tmp_path.glob("*.whl")))
  221. assert "foo.py" in wheel_files
  222. class TestNoConfig:
  223. DEFAULT_VERSION = "0.0.0" # Default version given by setuptools
  224. EXAMPLES = {
  225. "pkg1": ["src/pkg1.py"],
  226. "pkg2": ["src/pkg2/__init__.py"],
  227. "pkg3": ["src/pkg3/__init__.py", "src/pkg3-stubs/__init__.py"],
  228. "pkg4": ["pkg4/__init__.py", "pkg4-stubs/__init__.py"],
  229. "ns.nested.pkg1": ["src/ns/nested/pkg1/__init__.py"],
  230. "ns.nested.pkg2": ["ns/nested/pkg2/__init__.py"],
  231. }
  232. @pytest.mark.parametrize("example", EXAMPLES.keys())
  233. def test_discover_name(self, tmp_path, example):
  234. _populate_project_dir(tmp_path, self.EXAMPLES[example], {})
  235. dist = _get_dist(tmp_path, {})
  236. assert dist.get_name() == example
  237. def test_build_with_discovered_name(self, tmp_path):
  238. files = ["src/ns/nested/pkg/__init__.py"]
  239. _populate_project_dir(tmp_path, files, {})
  240. _run_build(tmp_path, "--sdist")
  241. # Expected distribution file
  242. dist_file = tmp_path / f"dist/ns_nested_pkg-{self.DEFAULT_VERSION}.tar.gz"
  243. assert dist_file.is_file()
  244. class TestWithAttrDirective:
  245. @pytest.mark.parametrize(
  246. ("folder", "opts"),
  247. [
  248. ("src", {}),
  249. ("lib", {"packages": "find:", "packages.find": {"where": "lib"}}),
  250. ],
  251. )
  252. def test_setupcfg_metadata(self, tmp_path, folder, opts):
  253. files = [f"{folder}/pkg/__init__.py", "setup.cfg"]
  254. _populate_project_dir(tmp_path, files, opts)
  255. config = (tmp_path / "setup.cfg").read_text(encoding="utf-8")
  256. overwrite = {
  257. folder: {"pkg": {"__init__.py": "version = 42"}},
  258. "setup.cfg": "[metadata]\nversion = attr: pkg.version\n" + config,
  259. }
  260. jaraco.path.build(overwrite, prefix=tmp_path)
  261. dist = _get_dist(tmp_path, {})
  262. assert dist.get_name() == "pkg"
  263. assert dist.get_version() == "42"
  264. assert dist.package_dir
  265. package_path = find_package_path("pkg", dist.package_dir, tmp_path)
  266. assert os.path.exists(package_path)
  267. assert folder in Path(package_path).parts()
  268. _run_build(tmp_path, "--sdist")
  269. dist_file = tmp_path / "dist/pkg-42.tar.gz"
  270. assert dist_file.is_file()
  271. def test_pyproject_metadata(self, tmp_path):
  272. _populate_project_dir(tmp_path, ["src/pkg/__init__.py"], {})
  273. overwrite = {
  274. "src": {"pkg": {"__init__.py": "version = 42"}},
  275. "pyproject.toml": (
  276. "[project]\nname = 'pkg'\ndynamic = ['version']\n"
  277. "[tool.setuptools.dynamic]\nversion = {attr = 'pkg.version'}\n"
  278. ),
  279. }
  280. jaraco.path.build(overwrite, prefix=tmp_path)
  281. dist = _get_dist(tmp_path, {})
  282. assert dist.get_version() == "42"
  283. assert dist.package_dir == {"": "src"}
  284. class TestWithCExtension:
  285. def _simulate_package_with_extension(self, tmp_path):
  286. # This example is based on: https://github.com/nucleic/kiwi/tree/1.4.0
  287. files = [
  288. "benchmarks/file.py",
  289. "docs/Makefile",
  290. "docs/requirements.txt",
  291. "docs/source/conf.py",
  292. "proj/header.h",
  293. "proj/file.py",
  294. "py/proj.cpp",
  295. "py/other.cpp",
  296. "py/file.py",
  297. "py/py.typed",
  298. "py/tests/test_proj.py",
  299. "README.rst",
  300. ]
  301. _populate_project_dir(tmp_path, files, {})
  302. setup_script = """
  303. from setuptools import Extension, setup
  304. ext_modules = [
  305. Extension(
  306. "proj",
  307. ["py/proj.cpp", "py/other.cpp"],
  308. include_dirs=["."],
  309. language="c++",
  310. ),
  311. ]
  312. setup(ext_modules=ext_modules)
  313. """
  314. (tmp_path / "setup.py").write_text(DALS(setup_script), encoding="utf-8")
  315. def test_skip_discovery_with_setupcfg_metadata(self, tmp_path):
  316. """Ensure that auto-discovery is not triggered when the project is based on
  317. C-extensions only, for backward compatibility.
  318. """
  319. self._simulate_package_with_extension(tmp_path)
  320. pyproject = """
  321. [build-system]
  322. requires = []
  323. build-backend = 'setuptools.build_meta'
  324. """
  325. (tmp_path / "pyproject.toml").write_text(DALS(pyproject), encoding="utf-8")
  326. setupcfg = """
  327. [metadata]
  328. name = proj
  329. version = 42
  330. """
  331. (tmp_path / "setup.cfg").write_text(DALS(setupcfg), encoding="utf-8")
  332. dist = _get_dist(tmp_path, {})
  333. assert dist.get_name() == "proj"
  334. assert dist.get_version() == "42"
  335. assert dist.py_modules is None
  336. assert dist.packages is None
  337. assert len(dist.ext_modules) == 1
  338. assert dist.ext_modules[0].name == "proj"
  339. def test_dont_skip_discovery_with_pyproject_metadata(self, tmp_path):
  340. """When opting-in to pyproject.toml metadata, auto-discovery will be active if
  341. the package lists C-extensions, but does not configure py-modules or packages.
  342. This way we ensure users with complex package layouts that would lead to the
  343. discovery of multiple top-level modules/packages see errors and are forced to
  344. explicitly set ``packages`` or ``py-modules``.
  345. """
  346. self._simulate_package_with_extension(tmp_path)
  347. pyproject = """
  348. [project]
  349. name = 'proj'
  350. version = '42'
  351. """
  352. (tmp_path / "pyproject.toml").write_text(DALS(pyproject), encoding="utf-8")
  353. with pytest.raises(PackageDiscoveryError, match="multiple (packages|modules)"):
  354. _get_dist(tmp_path, {})
  355. class TestWithPackageData:
  356. def _simulate_package_with_data_files(self, tmp_path, src_root):
  357. files = [
  358. f"{src_root}/proj/__init__.py",
  359. f"{src_root}/proj/file1.txt",
  360. f"{src_root}/proj/nested/file2.txt",
  361. ]
  362. _populate_project_dir(tmp_path, files, {})
  363. manifest = """
  364. global-include *.py *.txt
  365. """
  366. (tmp_path / "MANIFEST.in").write_text(DALS(manifest), encoding="utf-8")
  367. EXAMPLE_SETUPCFG = """
  368. [metadata]
  369. name = proj
  370. version = 42
  371. [options]
  372. include_package_data = True
  373. """
  374. EXAMPLE_PYPROJECT = """
  375. [project]
  376. name = "proj"
  377. version = "42"
  378. """
  379. PYPROJECT_PACKAGE_DIR = """
  380. [tool.setuptools]
  381. package-dir = {"" = "src"}
  382. """
  383. @pytest.mark.parametrize(
  384. ("src_root", "files"),
  385. [
  386. (".", {"setup.cfg": DALS(EXAMPLE_SETUPCFG)}),
  387. (".", {"pyproject.toml": DALS(EXAMPLE_PYPROJECT)}),
  388. ("src", {"setup.cfg": DALS(EXAMPLE_SETUPCFG)}),
  389. ("src", {"pyproject.toml": DALS(EXAMPLE_PYPROJECT)}),
  390. (
  391. "src",
  392. {
  393. "setup.cfg": DALS(EXAMPLE_SETUPCFG)
  394. + DALS(
  395. """
  396. packages = find:
  397. package_dir =
  398. =src
  399. [options.packages.find]
  400. where = src
  401. """
  402. )
  403. },
  404. ),
  405. (
  406. "src",
  407. {
  408. "pyproject.toml": DALS(EXAMPLE_PYPROJECT)
  409. + DALS(
  410. """
  411. [tool.setuptools]
  412. package-dir = {"" = "src"}
  413. """
  414. )
  415. },
  416. ),
  417. ],
  418. )
  419. def test_include_package_data(self, tmp_path, src_root, files):
  420. """
  421. Make sure auto-discovery does not affect package include_package_data.
  422. See issue #3196.
  423. """
  424. jaraco.path.build(files, prefix=str(tmp_path))
  425. self._simulate_package_with_data_files(tmp_path, src_root)
  426. expected = {
  427. os.path.normpath(f"{src_root}/proj/file1.txt").replace(os.sep, "/"),
  428. os.path.normpath(f"{src_root}/proj/nested/file2.txt").replace(os.sep, "/"),
  429. }
  430. _run_build(tmp_path)
  431. sdist_files = get_sdist_members(next(tmp_path.glob("dist/*.tar.gz")))
  432. print("~~~~~ sdist_members ~~~~~")
  433. print('\n'.join(sdist_files))
  434. assert sdist_files >= expected
  435. wheel_files = get_wheel_members(next(tmp_path.glob("dist/*.whl")))
  436. print("~~~~~ wheel_members ~~~~~")
  437. print('\n'.join(wheel_files))
  438. orig_files = {f.replace("src/", "").replace("lib/", "") for f in expected}
  439. assert wheel_files >= orig_files
  440. def test_compatible_with_numpy_configuration(tmp_path):
  441. files = [
  442. "dir1/__init__.py",
  443. "dir2/__init__.py",
  444. "file.py",
  445. ]
  446. _populate_project_dir(tmp_path, files, {})
  447. dist = Distribution({})
  448. dist.configuration = object()
  449. dist.set_defaults()
  450. assert dist.py_modules is None
  451. assert dist.packages is None
  452. def test_name_discovery_doesnt_break_cli(tmpdir_cwd):
  453. jaraco.path.build({"pkg.py": ""})
  454. dist = Distribution({})
  455. dist.script_args = ["--name"]
  456. dist.set_defaults()
  457. dist.parse_command_line() # <-- no exception should be raised here.
  458. assert dist.get_name() == "pkg"
  459. def test_preserve_explicit_name_with_dynamic_version(tmpdir_cwd, monkeypatch):
  460. """According to #3545 it seems that ``name`` discovery is running,
  461. even when the project already explicitly sets it.
  462. This seems to be related to parsing of dynamic versions (via ``attr`` directive),
  463. which requires the auto-discovery of ``package_dir``.
  464. """
  465. files = {
  466. "src": {
  467. "pkg": {"__init__.py": "__version__ = 42\n"},
  468. },
  469. "pyproject.toml": DALS(
  470. """
  471. [project]
  472. name = "myproj" # purposefully different from package name
  473. dynamic = ["version"]
  474. [tool.setuptools.dynamic]
  475. version = {"attr" = "pkg.__version__"}
  476. """
  477. ),
  478. }
  479. jaraco.path.build(files)
  480. dist = Distribution({})
  481. orig_analyse_name = dist.set_defaults.analyse_name
  482. def spy_analyse_name():
  483. # We can check if name discovery was triggered by ensuring the original
  484. # name remains instead of the package name.
  485. orig_analyse_name()
  486. assert dist.get_name() == "myproj"
  487. monkeypatch.setattr(dist.set_defaults, "analyse_name", spy_analyse_name)
  488. dist.parse_config_files()
  489. assert dist.get_version() == "42"
  490. assert set(dist.packages) == {"pkg"}
  491. def _populate_project_dir(root, files, options):
  492. # NOTE: Currently pypa/build will refuse to build the project if no
  493. # `pyproject.toml` or `setup.py` is found. So it is impossible to do
  494. # completely "config-less" projects.
  495. basic = {
  496. "setup.py": "import setuptools\nsetuptools.setup()",
  497. "README.md": "# Example Package",
  498. "LICENSE": "Copyright (c) 2018",
  499. }
  500. jaraco.path.build(basic, prefix=root)
  501. _write_setupcfg(root, options)
  502. paths = (root / f for f in files)
  503. for path in paths:
  504. path.parent.mkdir(exist_ok=True, parents=True)
  505. path.touch()
  506. def _write_setupcfg(root, options):
  507. if not options:
  508. print("~~~~~ **NO** setup.cfg ~~~~~")
  509. return
  510. setupcfg = ConfigParser()
  511. setupcfg.add_section("options")
  512. for key, value in options.items():
  513. if key == "packages.find":
  514. setupcfg.add_section(f"options.{key}")
  515. setupcfg[f"options.{key}"].update(value)
  516. elif isinstance(value, list):
  517. setupcfg["options"][key] = ", ".join(value)
  518. elif isinstance(value, dict):
  519. str_value = "\n".join(f"\t{k} = {v}" for k, v in value.items())
  520. setupcfg["options"][key] = "\n" + str_value
  521. else:
  522. setupcfg["options"][key] = str(value)
  523. with open(root / "setup.cfg", "w", encoding="utf-8") as f:
  524. setupcfg.write(f)
  525. print("~~~~~ setup.cfg ~~~~~")
  526. print((root / "setup.cfg").read_text(encoding="utf-8"))
  527. def _run_build(path, *flags):
  528. cmd = [sys.executable, "-m", "build", "--no-isolation", *flags, str(path)]
  529. return run(cmd, env={'DISTUTILS_DEBUG': ''})
  530. def _get_dist(dist_path, attrs):
  531. root = "/".join(os.path.split(dist_path)) # POSIX-style
  532. script = dist_path / 'setup.py'
  533. if script.exists():
  534. with Path(dist_path):
  535. dist = cast(
  536. Distribution,
  537. distutils.core.run_setup("setup.py", {}, stop_after="init"),
  538. )
  539. else:
  540. dist = Distribution(attrs)
  541. dist.src_root = root
  542. dist.script_name = "setup.py"
  543. with Path(dist_path):
  544. dist.parse_config_files()
  545. dist.set_defaults()
  546. return dist
  547. def _run_sdist_programatically(dist_path, attrs):
  548. dist = _get_dist(dist_path, attrs)
  549. cmd = sdist(dist)
  550. cmd.ensure_finalized()
  551. assert cmd.distribution.packages or cmd.distribution.py_modules
  552. with quiet(), Path(dist_path):
  553. cmd.run()
  554. return dist, cmd