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