fixtures.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  1. import contextlib
  2. import io
  3. import os
  4. import subprocess
  5. import sys
  6. import tarfile
  7. import time
  8. from pathlib import Path
  9. import jaraco.path
  10. import path
  11. import pytest
  12. from setuptools._normalization import safer_name
  13. from . import contexts, environment
  14. from .textwrap import DALS
  15. @pytest.fixture
  16. def user_override(monkeypatch):
  17. """
  18. Override site.USER_BASE and site.USER_SITE with temporary directories in
  19. a context.
  20. """
  21. with contexts.tempdir() as user_base:
  22. monkeypatch.setattr('site.USER_BASE', user_base)
  23. with contexts.tempdir() as user_site:
  24. monkeypatch.setattr('site.USER_SITE', user_site)
  25. with contexts.save_user_site_setting():
  26. yield
  27. @pytest.fixture
  28. def tmpdir_cwd(tmpdir):
  29. with tmpdir.as_cwd() as orig:
  30. yield orig
  31. @pytest.fixture(autouse=True, scope="session")
  32. def workaround_xdist_376(request):
  33. """
  34. Workaround pytest-dev/pytest-xdist#376
  35. ``pytest-xdist`` tends to inject '' into ``sys.path``,
  36. which may break certain isolation expectations.
  37. Remove the entry so the import
  38. machinery behaves the same irrespective of xdist.
  39. """
  40. if not request.config.pluginmanager.has_plugin('xdist'):
  41. return
  42. with contextlib.suppress(ValueError):
  43. sys.path.remove('')
  44. @pytest.fixture
  45. def sample_project(tmp_path):
  46. """
  47. Clone the 'sampleproject' and return a path to it.
  48. """
  49. cmd = ['git', 'clone', 'https://github.com/pypa/sampleproject']
  50. try:
  51. subprocess.check_call(cmd, cwd=str(tmp_path))
  52. except Exception:
  53. pytest.skip("Unable to clone sampleproject")
  54. return tmp_path / 'sampleproject'
  55. @pytest.fixture
  56. def sample_project_cwd(sample_project):
  57. with path.Path(sample_project):
  58. yield
  59. # sdist and wheel artifacts should be stable across a round of tests
  60. # so we can build them once per session and use the files as "readonly"
  61. # In the case of setuptools, building the wheel without sdist may cause
  62. # it to contain the `build` directory, and therefore create situations with
  63. # `setuptools/build/lib/build/lib/...`. To avoid that, build both artifacts at once.
  64. def _build_distributions(tmp_path_factory, request):
  65. with contexts.session_locked_tmp_dir(
  66. request, tmp_path_factory, "dist_build"
  67. ) as tmp: # pragma: no cover
  68. sdist = next(tmp.glob("*.tar.gz"), None)
  69. wheel = next(tmp.glob("*.whl"), None)
  70. if sdist and wheel:
  71. return (sdist, wheel)
  72. # Sanity check: should not create recursive setuptools/build/lib/build/lib/...
  73. assert not Path(request.config.rootdir, "build/lib/build").exists()
  74. subprocess.check_output([
  75. sys.executable,
  76. "-m",
  77. "build",
  78. "--outdir",
  79. str(tmp),
  80. str(request.config.rootdir),
  81. ])
  82. # Sanity check: should not create recursive setuptools/build/lib/build/lib/...
  83. assert not Path(request.config.rootdir, "build/lib/build").exists()
  84. return next(tmp.glob("*.tar.gz")), next(tmp.glob("*.whl"))
  85. @pytest.fixture(scope="session")
  86. def setuptools_sdist(tmp_path_factory, request):
  87. prebuilt = os.getenv("PRE_BUILT_SETUPTOOLS_SDIST")
  88. if prebuilt and os.path.exists(prebuilt): # pragma: no cover
  89. return Path(prebuilt).resolve()
  90. sdist, _ = _build_distributions(tmp_path_factory, request)
  91. return sdist
  92. @pytest.fixture(scope="session")
  93. def setuptools_wheel(tmp_path_factory, request):
  94. prebuilt = os.getenv("PRE_BUILT_SETUPTOOLS_WHEEL")
  95. if prebuilt and os.path.exists(prebuilt): # pragma: no cover
  96. return Path(prebuilt).resolve()
  97. _, wheel = _build_distributions(tmp_path_factory, request)
  98. return wheel
  99. @pytest.fixture
  100. def venv(tmp_path, setuptools_wheel):
  101. """Virtual env with the version of setuptools under test installed"""
  102. env = environment.VirtualEnv()
  103. env.root = path.Path(tmp_path / 'venv')
  104. env.create_opts = ['--no-setuptools', '--wheel=bundle']
  105. # TODO: Use `--no-wheel` when setuptools implements its own bdist_wheel
  106. env.req = str(setuptools_wheel)
  107. # In some environments (eg. downstream distro packaging),
  108. # where tox isn't used to run tests and PYTHONPATH is set to point to
  109. # a specific setuptools codebase, PYTHONPATH will leak into the spawned
  110. # processes.
  111. # env.create() should install the just created setuptools
  112. # wheel, but it doesn't if it finds another existing matching setuptools
  113. # installation present on PYTHONPATH:
  114. # `setuptools is already installed with the same version as the provided
  115. # wheel. Use --force-reinstall to force an installation of the wheel.`
  116. # This prevents leaking PYTHONPATH to the created environment.
  117. with contexts.environment(PYTHONPATH=None):
  118. return env.create()
  119. @pytest.fixture
  120. def venv_without_setuptools(tmp_path):
  121. """Virtual env without any version of setuptools installed"""
  122. env = environment.VirtualEnv()
  123. env.root = path.Path(tmp_path / 'venv_without_setuptools')
  124. env.create_opts = ['--no-setuptools', '--no-wheel']
  125. env.ensure_env()
  126. return env
  127. @pytest.fixture
  128. def bare_venv(tmp_path):
  129. """Virtual env without any common packages installed"""
  130. env = environment.VirtualEnv()
  131. env.root = path.Path(tmp_path / 'bare_venv')
  132. env.create_opts = ['--no-setuptools', '--no-pip', '--no-wheel', '--no-seed']
  133. env.ensure_env()
  134. return env
  135. def make_sdist(dist_path, files):
  136. """
  137. Create a simple sdist tarball at dist_path, containing the files
  138. listed in ``files`` as ``(filename, content)`` tuples.
  139. """
  140. # Distributions with only one file don't play well with pip.
  141. assert len(files) > 1
  142. with tarfile.open(dist_path, 'w:gz') as dist:
  143. for filename, content in files:
  144. file_bytes = io.BytesIO(content.encode('utf-8'))
  145. file_info = tarfile.TarInfo(name=filename)
  146. file_info.size = len(file_bytes.getvalue())
  147. file_info.mtime = int(time.time())
  148. dist.addfile(file_info, fileobj=file_bytes)
  149. def make_trivial_sdist(dist_path, distname, version, setuptools_wheel=None):
  150. """
  151. Create a simple sdist tarball at dist_path, containing just a simple
  152. setup.py.
  153. If ``setuptools_wheel`` is passed, a ``pyproject.toml`` file will also
  154. be generated and the passed value will be used as location for
  155. setuptools (as build dependency).
  156. """
  157. files = [
  158. (
  159. 'setup.py',
  160. DALS(
  161. f"""\
  162. import setuptools
  163. setuptools.setup(
  164. name={distname!r},
  165. version={version!r}
  166. )
  167. """
  168. ),
  169. ),
  170. ('setup.cfg', ''),
  171. ]
  172. if setuptools_wheel:
  173. files.append((
  174. "pyproject.toml",
  175. DALS(
  176. f"""\
  177. [build-system]
  178. requires = ["setuptools @ {setuptools_wheel.as_uri()}"]
  179. build-backend = "setuptools.build_meta"
  180. """
  181. ),
  182. ))
  183. make_sdist(dist_path, files)
  184. def make_nspkg_sdist(dist_path, distname, version):
  185. """
  186. Make an sdist tarball with distname and version which also contains one
  187. package with the same name as distname. The top-level package is
  188. designated a namespace package).
  189. """
  190. # Assert that the distname contains at least one period
  191. assert '.' in distname
  192. parts = distname.split('.')
  193. nspackage = parts[0]
  194. packages = ['.'.join(parts[:idx]) for idx in range(1, len(parts) + 1)]
  195. setup_py = DALS(
  196. f"""\
  197. import setuptools
  198. setuptools.setup(
  199. name={distname!r},
  200. version={version!r},
  201. packages={packages!r},
  202. namespace_packages=[{nspackage!r}]
  203. )
  204. """
  205. )
  206. init = "__import__('pkg_resources').declare_namespace(__name__)"
  207. files = [('setup.py', setup_py), (os.path.join(nspackage, '__init__.py'), init)]
  208. for package in packages[1:]:
  209. filename = os.path.join(*(package.split('.') + ['__init__.py']))
  210. files.append((filename, ''))
  211. make_sdist(dist_path, files)
  212. def make_python_requires_sdist(dist_path, distname, version, python_requires):
  213. make_sdist(
  214. dist_path,
  215. [
  216. (
  217. 'setup.py',
  218. DALS(
  219. """\
  220. import setuptools
  221. setuptools.setup(
  222. name={name!r},
  223. version={version!r},
  224. python_requires={python_requires!r},
  225. )
  226. """
  227. ).format(
  228. name=distname, version=version, python_requires=python_requires
  229. ),
  230. ),
  231. ('setup.cfg', ''),
  232. ],
  233. )
  234. def create_setup_requires_package(
  235. path,
  236. distname='foobar',
  237. version='0.1',
  238. make_package=make_trivial_sdist,
  239. setup_py_template=None,
  240. setup_attrs=None,
  241. use_setup_cfg=(),
  242. ):
  243. """Creates a source tree under path for a trivial test package that has a
  244. single requirement in setup_requires--a tarball for that requirement is
  245. also created and added to the dependency_links argument.
  246. ``distname`` and ``version`` refer to the name/version of the package that
  247. the test package requires via ``setup_requires``. The name of the test
  248. package itself is just 'test_pkg'.
  249. """
  250. normalized_distname = safer_name(distname)
  251. test_setup_attrs = {
  252. 'name': 'test_pkg',
  253. 'version': '0.0',
  254. 'setup_requires': [f'{normalized_distname}=={version}'],
  255. 'dependency_links': [os.path.abspath(path)],
  256. }
  257. if setup_attrs:
  258. test_setup_attrs.update(setup_attrs)
  259. test_pkg = os.path.join(path, 'test_pkg')
  260. os.mkdir(test_pkg)
  261. # setup.cfg
  262. if use_setup_cfg:
  263. options = []
  264. metadata = []
  265. for name in use_setup_cfg:
  266. value = test_setup_attrs.pop(name)
  267. if name in 'name version'.split():
  268. section = metadata
  269. else:
  270. section = options
  271. if isinstance(value, (tuple, list)):
  272. value = ';'.join(value)
  273. section.append(f'{name}: {value}')
  274. test_setup_cfg_contents = DALS(
  275. """
  276. [metadata]
  277. {metadata}
  278. [options]
  279. {options}
  280. """
  281. ).format(
  282. options='\n'.join(options),
  283. metadata='\n'.join(metadata),
  284. )
  285. else:
  286. test_setup_cfg_contents = ''
  287. with open(os.path.join(test_pkg, 'setup.cfg'), 'w', encoding="utf-8") as f:
  288. f.write(test_setup_cfg_contents)
  289. # setup.py
  290. if setup_py_template is None:
  291. setup_py_template = DALS(
  292. """\
  293. import setuptools
  294. setuptools.setup(**%r)
  295. """
  296. )
  297. with open(os.path.join(test_pkg, 'setup.py'), 'w', encoding="utf-8") as f:
  298. f.write(setup_py_template % test_setup_attrs)
  299. foobar_path = os.path.join(path, f'{normalized_distname}-{version}.tar.gz')
  300. make_package(foobar_path, distname, version)
  301. return test_pkg
  302. @pytest.fixture
  303. def pbr_package(tmp_path, monkeypatch, venv):
  304. files = {
  305. "pyproject.toml": DALS(
  306. """
  307. [build-system]
  308. requires = ["setuptools"]
  309. build-backend = "setuptools.build_meta"
  310. """
  311. ),
  312. "setup.py": DALS(
  313. """
  314. __import__('setuptools').setup(
  315. pbr=True,
  316. setup_requires=["pbr"],
  317. )
  318. """
  319. ),
  320. "setup.cfg": DALS(
  321. """
  322. [metadata]
  323. name = mypkg
  324. [files]
  325. packages =
  326. mypkg
  327. """
  328. ),
  329. "mypkg": {
  330. "__init__.py": "",
  331. "hello.py": "print('Hello world!')",
  332. },
  333. "other": {"test.txt": "Another file in here."},
  334. }
  335. venv.run(["python", "-m", "pip", "install", "pbr"])
  336. prefix = tmp_path / 'mypkg'
  337. prefix.mkdir()
  338. jaraco.path.build(files, prefix=prefix)
  339. monkeypatch.setenv('PBR_VERSION', "0.42")
  340. return prefix