1import os 2 3import pytest 4 5from distutils.errors import DistutilsOptionError 6from setuptools.config import expand 7from setuptools.discovery import find_package_path 8 9 10def write_files(files, root_dir): 11 for file, content in files.items(): 12 path = root_dir / file 13 path.parent.mkdir(exist_ok=True, parents=True) 14 path.write_text(content) 15 16 17def test_glob_relative(tmp_path, monkeypatch): 18 files = { 19 "dir1/dir2/dir3/file1.txt", 20 "dir1/dir2/file2.txt", 21 "dir1/file3.txt", 22 "a.ini", 23 "b.ini", 24 "dir1/c.ini", 25 "dir1/dir2/a.ini", 26 } 27 28 write_files({k: "" for k in files}, tmp_path) 29 patterns = ["**/*.txt", "[ab].*", "**/[ac].ini"] 30 monkeypatch.chdir(tmp_path) 31 assert set(expand.glob_relative(patterns)) == files 32 # Make sure the same APIs work outside cwd 33 assert set(expand.glob_relative(patterns, tmp_path)) == files 34 35 36def test_read_files(tmp_path, monkeypatch): 37 38 dir_ = tmp_path / "dir_" 39 (tmp_path / "_dir").mkdir(exist_ok=True) 40 (tmp_path / "a.txt").touch() 41 files = { 42 "a.txt": "a", 43 "dir1/b.txt": "b", 44 "dir1/dir2/c.txt": "c" 45 } 46 write_files(files, dir_) 47 48 with monkeypatch.context() as m: 49 m.chdir(dir_) 50 assert expand.read_files(list(files)) == "a\nb\nc" 51 52 cannot_access_msg = r"Cannot access '.*\.\..a\.txt'" 53 with pytest.raises(DistutilsOptionError, match=cannot_access_msg): 54 expand.read_files(["../a.txt"]) 55 56 # Make sure the same APIs work outside cwd 57 assert expand.read_files(list(files), dir_) == "a\nb\nc" 58 with pytest.raises(DistutilsOptionError, match=cannot_access_msg): 59 expand.read_files(["../a.txt"], dir_) 60 61 62class TestReadAttr: 63 def test_read_attr(self, tmp_path, monkeypatch): 64 files = { 65 "pkg/__init__.py": "", 66 "pkg/sub/__init__.py": "VERSION = '0.1.1'", 67 "pkg/sub/mod.py": ( 68 "VALUES = {'a': 0, 'b': {42}, 'c': (0, 1, 1)}\n" 69 "raise SystemExit(1)" 70 ), 71 } 72 write_files(files, tmp_path) 73 74 with monkeypatch.context() as m: 75 m.chdir(tmp_path) 76 # Make sure it can read the attr statically without evaluating the module 77 assert expand.read_attr('pkg.sub.VERSION') == '0.1.1' 78 values = expand.read_attr('lib.mod.VALUES', {'lib': 'pkg/sub'}) 79 80 assert values['a'] == 0 81 assert values['b'] == {42} 82 83 # Make sure the same APIs work outside cwd 84 assert expand.read_attr('pkg.sub.VERSION', root_dir=tmp_path) == '0.1.1' 85 values = expand.read_attr('lib.mod.VALUES', {'lib': 'pkg/sub'}, tmp_path) 86 assert values['c'] == (0, 1, 1) 87 88 def test_import_order(self, tmp_path): 89 """ 90 Sometimes the import machinery will import the parent package of a nested 91 module, which triggers side-effects and might create problems (see issue #3176) 92 93 ``read_attr`` should bypass these limitations by resolving modules statically 94 (via ast.literal_eval). 95 """ 96 files = { 97 "src/pkg/__init__.py": "from .main import func\nfrom .about import version", 98 "src/pkg/main.py": "import super_complicated_dep\ndef func(): return 42", 99 "src/pkg/about.py": "version = '42'", 100 } 101 write_files(files, tmp_path) 102 attr_desc = "pkg.about.version" 103 package_dir = {"": "src"} 104 # `import super_complicated_dep` should not run, otherwise the build fails 105 assert expand.read_attr(attr_desc, package_dir, tmp_path) == "42" 106 107 108@pytest.mark.parametrize( 109 'package_dir, file, module, return_value', 110 [ 111 ({"": "src"}, "src/pkg/main.py", "pkg.main", 42), 112 ({"pkg": "lib"}, "lib/main.py", "pkg.main", 13), 113 ({}, "single_module.py", "single_module", 70), 114 ({}, "flat_layout/pkg.py", "flat_layout.pkg", 836), 115 ] 116) 117def test_resolve_class(tmp_path, package_dir, file, module, return_value): 118 files = {file: f"class Custom:\n def testing(self): return {return_value}"} 119 write_files(files, tmp_path) 120 cls = expand.resolve_class(f"{module}.Custom", package_dir, tmp_path) 121 assert cls().testing() == return_value 122 123 124@pytest.mark.parametrize( 125 'args, pkgs', 126 [ 127 ({"where": ["."], "namespaces": False}, {"pkg", "other"}), 128 ({"where": [".", "dir1"], "namespaces": False}, {"pkg", "other", "dir2"}), 129 ({"namespaces": True}, {"pkg", "other", "dir1", "dir1.dir2"}), 130 ({}, {"pkg", "other", "dir1", "dir1.dir2"}), # default value for `namespaces` 131 ] 132) 133def test_find_packages(tmp_path, monkeypatch, args, pkgs): 134 files = { 135 "pkg/__init__.py", 136 "other/__init__.py", 137 "dir1/dir2/__init__.py", 138 } 139 write_files({k: "" for k in files}, tmp_path) 140 141 package_dir = {} 142 kwargs = {"root_dir": tmp_path, "fill_package_dir": package_dir, **args} 143 where = kwargs.get("where", ["."]) 144 assert set(expand.find_packages(**kwargs)) == pkgs 145 for pkg in pkgs: 146 pkg_path = find_package_path(pkg, package_dir, tmp_path) 147 assert os.path.exists(pkg_path) 148 149 # Make sure the same APIs work outside cwd 150 where = [ 151 str((tmp_path / p).resolve()).replace(os.sep, "/") # ensure posix-style paths 152 for p in args.pop("where", ["."]) 153 ] 154 155 assert set(expand.find_packages(where=where, **args)) == pkgs 156