test_expand.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. import os
  2. import sys
  3. from pathlib import Path
  4. import pytest
  5. from setuptools._static import is_static
  6. from setuptools.config import expand
  7. from setuptools.discovery import find_package_path
  8. from distutils.errors import DistutilsOptionError
  9. def write_files(files, root_dir):
  10. for file, content in files.items():
  11. path = root_dir / file
  12. path.parent.mkdir(exist_ok=True, parents=True)
  13. path.write_text(content, encoding="utf-8")
  14. def test_glob_relative(tmp_path, monkeypatch):
  15. files = {
  16. "dir1/dir2/dir3/file1.txt",
  17. "dir1/dir2/file2.txt",
  18. "dir1/file3.txt",
  19. "a.ini",
  20. "b.ini",
  21. "dir1/c.ini",
  22. "dir1/dir2/a.ini",
  23. }
  24. write_files({k: "" for k in files}, tmp_path)
  25. patterns = ["**/*.txt", "[ab].*", "**/[ac].ini"]
  26. monkeypatch.chdir(tmp_path)
  27. assert set(expand.glob_relative(patterns)) == files
  28. # Make sure the same APIs work outside cwd
  29. assert set(expand.glob_relative(patterns, tmp_path)) == files
  30. def test_read_files(tmp_path, monkeypatch):
  31. dir_ = tmp_path / "dir_"
  32. (tmp_path / "_dir").mkdir(exist_ok=True)
  33. (tmp_path / "a.txt").touch()
  34. files = {"a.txt": "a", "dir1/b.txt": "b", "dir1/dir2/c.txt": "c"}
  35. write_files(files, dir_)
  36. secrets = Path(str(dir_) + "secrets")
  37. secrets.mkdir(exist_ok=True)
  38. write_files({"secrets.txt": "secret keys"}, secrets)
  39. with monkeypatch.context() as m:
  40. m.chdir(dir_)
  41. assert expand.read_files(list(files)) == "a\nb\nc"
  42. cannot_access_msg = r"Cannot access '.*\.\..a\.txt'"
  43. with pytest.raises(DistutilsOptionError, match=cannot_access_msg):
  44. expand.read_files(["../a.txt"])
  45. cannot_access_secrets_msg = r"Cannot access '.*secrets\.txt'"
  46. with pytest.raises(DistutilsOptionError, match=cannot_access_secrets_msg):
  47. expand.read_files(["../dir_secrets/secrets.txt"])
  48. # Make sure the same APIs work outside cwd
  49. assert expand.read_files(list(files), dir_) == "a\nb\nc"
  50. with pytest.raises(DistutilsOptionError, match=cannot_access_msg):
  51. expand.read_files(["../a.txt"], dir_)
  52. class TestReadAttr:
  53. @pytest.mark.parametrize(
  54. "example",
  55. [
  56. # No cookie means UTF-8:
  57. b"__version__ = '\xc3\xa9'\nraise SystemExit(1)\n",
  58. # If a cookie is present, honor it:
  59. b"# -*- coding: utf-8 -*-\n__version__ = '\xc3\xa9'\nraise SystemExit(1)\n",
  60. b"# -*- coding: latin1 -*-\n__version__ = '\xe9'\nraise SystemExit(1)\n",
  61. ],
  62. )
  63. def test_read_attr_encoding_cookie(self, example, tmp_path):
  64. (tmp_path / "mod.py").write_bytes(example)
  65. assert expand.read_attr('mod.__version__', root_dir=tmp_path) == 'é'
  66. def test_read_attr(self, tmp_path, monkeypatch):
  67. files = {
  68. "pkg/__init__.py": "",
  69. "pkg/sub/__init__.py": "VERSION = '0.1.1'",
  70. "pkg/sub/mod.py": (
  71. "VALUES = {'a': 0, 'b': {42}, 'c': (0, 1, 1)}\nraise SystemExit(1)"
  72. ),
  73. }
  74. write_files(files, tmp_path)
  75. with monkeypatch.context() as m:
  76. m.chdir(tmp_path)
  77. # Make sure it can read the attr statically without evaluating the module
  78. version = expand.read_attr('pkg.sub.VERSION')
  79. values = expand.read_attr('lib.mod.VALUES', {'lib': 'pkg/sub'})
  80. assert version == '0.1.1'
  81. assert is_static(values)
  82. assert values['a'] == 0
  83. assert values['b'] == {42}
  84. assert is_static(values)
  85. # Make sure the same APIs work outside cwd
  86. assert expand.read_attr('pkg.sub.VERSION', root_dir=tmp_path) == '0.1.1'
  87. values = expand.read_attr('lib.mod.VALUES', {'lib': 'pkg/sub'}, tmp_path)
  88. assert values['c'] == (0, 1, 1)
  89. @pytest.mark.parametrize(
  90. "example",
  91. [
  92. "VERSION: str\nVERSION = '0.1.1'\nraise SystemExit(1)\n",
  93. "VERSION: str = '0.1.1'\nraise SystemExit(1)\n",
  94. ],
  95. )
  96. def test_read_annotated_attr(self, tmp_path, example):
  97. files = {
  98. "pkg/__init__.py": "",
  99. "pkg/sub/__init__.py": example,
  100. }
  101. write_files(files, tmp_path)
  102. # Make sure this attribute can be read statically
  103. version = expand.read_attr('pkg.sub.VERSION', root_dir=tmp_path)
  104. assert version == '0.1.1'
  105. assert is_static(version)
  106. @pytest.mark.parametrize(
  107. "example",
  108. [
  109. "VERSION = (lambda: '0.1.1')()\n",
  110. "def fn(): return '0.1.1'\nVERSION = fn()\n",
  111. "VERSION: str = (lambda: '0.1.1')()\n",
  112. ],
  113. )
  114. def test_read_dynamic_attr(self, tmp_path, monkeypatch, example):
  115. files = {
  116. "pkg/__init__.py": "",
  117. "pkg/sub/__init__.py": example,
  118. }
  119. write_files(files, tmp_path)
  120. monkeypatch.chdir(tmp_path)
  121. version = expand.read_attr('pkg.sub.VERSION')
  122. assert version == '0.1.1'
  123. assert not is_static(version)
  124. def test_import_order(self, tmp_path):
  125. """
  126. Sometimes the import machinery will import the parent package of a nested
  127. module, which triggers side-effects and might create problems (see issue #3176)
  128. ``read_attr`` should bypass these limitations by resolving modules statically
  129. (via ast.literal_eval).
  130. """
  131. files = {
  132. "src/pkg/__init__.py": "from .main import func\nfrom .about import version",
  133. "src/pkg/main.py": "import super_complicated_dep\ndef func(): return 42",
  134. "src/pkg/about.py": "version = '42'",
  135. }
  136. write_files(files, tmp_path)
  137. attr_desc = "pkg.about.version"
  138. package_dir = {"": "src"}
  139. # `import super_complicated_dep` should not run, otherwise the build fails
  140. assert expand.read_attr(attr_desc, package_dir, tmp_path) == "42"
  141. @pytest.mark.parametrize(
  142. ("package_dir", "file", "module", "return_value"),
  143. [
  144. ({"": "src"}, "src/pkg/main.py", "pkg.main", 42),
  145. ({"pkg": "lib"}, "lib/main.py", "pkg.main", 13),
  146. ({}, "single_module.py", "single_module", 70),
  147. ({}, "flat_layout/pkg.py", "flat_layout.pkg", 836),
  148. ],
  149. )
  150. def test_resolve_class(monkeypatch, tmp_path, package_dir, file, module, return_value):
  151. monkeypatch.setattr(sys, "modules", {}) # reproducibility
  152. files = {file: f"class Custom:\n def testing(self): return {return_value}"}
  153. write_files(files, tmp_path)
  154. cls = expand.resolve_class(f"{module}.Custom", package_dir, tmp_path)
  155. assert cls().testing() == return_value
  156. @pytest.mark.parametrize(
  157. ("args", "pkgs"),
  158. [
  159. ({"where": ["."], "namespaces": False}, {"pkg", "other"}),
  160. ({"where": [".", "dir1"], "namespaces": False}, {"pkg", "other", "dir2"}),
  161. ({"namespaces": True}, {"pkg", "other", "dir1", "dir1.dir2"}),
  162. ({}, {"pkg", "other", "dir1", "dir1.dir2"}), # default value for `namespaces`
  163. ],
  164. )
  165. def test_find_packages(tmp_path, args, pkgs):
  166. files = {
  167. "pkg/__init__.py",
  168. "other/__init__.py",
  169. "dir1/dir2/__init__.py",
  170. }
  171. write_files({k: "" for k in files}, tmp_path)
  172. package_dir = {}
  173. kwargs = {"root_dir": tmp_path, "fill_package_dir": package_dir, **args}
  174. where = kwargs.get("where", ["."])
  175. assert set(expand.find_packages(**kwargs)) == pkgs
  176. for pkg in pkgs:
  177. pkg_path = find_package_path(pkg, package_dir, tmp_path)
  178. assert os.path.exists(pkg_path)
  179. # Make sure the same APIs work outside cwd
  180. where = [
  181. str((tmp_path / p).resolve()).replace(os.sep, "/") # ensure posix-style paths
  182. for p in args.pop("where", ["."])
  183. ]
  184. assert set(expand.find_packages(where=where, **args)) == pkgs
  185. @pytest.mark.parametrize(
  186. ("files", "where", "expected_package_dir"),
  187. [
  188. (["pkg1/__init__.py", "pkg1/other.py"], ["."], {}),
  189. (["pkg1/__init__.py", "pkg2/__init__.py"], ["."], {}),
  190. (["src/pkg1/__init__.py", "src/pkg1/other.py"], ["src"], {"": "src"}),
  191. (["src/pkg1/__init__.py", "src/pkg2/__init__.py"], ["src"], {"": "src"}),
  192. (
  193. ["src1/pkg1/__init__.py", "src2/pkg2/__init__.py"],
  194. ["src1", "src2"],
  195. {"pkg1": "src1/pkg1", "pkg2": "src2/pkg2"},
  196. ),
  197. (
  198. ["src/pkg1/__init__.py", "pkg2/__init__.py"],
  199. ["src", "."],
  200. {"pkg1": "src/pkg1"},
  201. ),
  202. ],
  203. )
  204. def test_fill_package_dir(tmp_path, files, where, expected_package_dir):
  205. write_files({k: "" for k in files}, tmp_path)
  206. pkg_dir = {}
  207. kwargs = {"root_dir": tmp_path, "fill_package_dir": pkg_dir, "namespaces": False}
  208. pkgs = expand.find_packages(where=where, **kwargs)
  209. assert set(pkg_dir.items()) == set(expected_package_dir.items())
  210. for pkg in pkgs:
  211. pkg_path = find_package_path(pkg, pkg_dir, tmp_path)
  212. assert os.path.exists(pkg_path)