1"""Tests for automatic package discovery"""
2import os
3import sys
4import shutil
5import tempfile
6import platform
7
8import pytest
9
10from setuptools import find_packages
11from setuptools import find_namespace_packages
12from setuptools.discovery import FlatLayoutPackageFinder
13
14
15# modeled after CPython's test.support.can_symlink
16def can_symlink():
17    TESTFN = tempfile.mktemp()
18    symlink_path = TESTFN + "can_symlink"
19    try:
20        os.symlink(TESTFN, symlink_path)
21        can = True
22    except (OSError, NotImplementedError, AttributeError):
23        can = False
24    else:
25        os.remove(symlink_path)
26    globals().update(can_symlink=lambda: can)
27    return can
28
29
30def has_symlink():
31    bad_symlink = (
32        # Windows symlink directory detection is broken on Python 3.2
33        platform.system() == 'Windows' and sys.version_info[:2] == (3, 2)
34    )
35    return can_symlink() and not bad_symlink
36
37
38class TestFindPackages:
39    def setup_method(self, method):
40        self.dist_dir = tempfile.mkdtemp()
41        self._make_pkg_structure()
42
43    def teardown_method(self, method):
44        shutil.rmtree(self.dist_dir)
45
46    def _make_pkg_structure(self):
47        """Make basic package structure.
48
49        dist/
50            docs/
51                conf.py
52            pkg/
53                __pycache__/
54                nspkg/
55                    mod.py
56                subpkg/
57                    assets/
58                        asset
59                    __init__.py
60            setup.py
61
62        """
63        self.docs_dir = self._mkdir('docs', self.dist_dir)
64        self._touch('conf.py', self.docs_dir)
65        self.pkg_dir = self._mkdir('pkg', self.dist_dir)
66        self._mkdir('__pycache__', self.pkg_dir)
67        self.ns_pkg_dir = self._mkdir('nspkg', self.pkg_dir)
68        self._touch('mod.py', self.ns_pkg_dir)
69        self.sub_pkg_dir = self._mkdir('subpkg', self.pkg_dir)
70        self.asset_dir = self._mkdir('assets', self.sub_pkg_dir)
71        self._touch('asset', self.asset_dir)
72        self._touch('__init__.py', self.sub_pkg_dir)
73        self._touch('setup.py', self.dist_dir)
74
75    def _mkdir(self, path, parent_dir=None):
76        if parent_dir:
77            path = os.path.join(parent_dir, path)
78        os.mkdir(path)
79        return path
80
81    def _touch(self, path, dir_=None):
82        if dir_:
83            path = os.path.join(dir_, path)
84        fp = open(path, 'w')
85        fp.close()
86        return path
87
88    def test_regular_package(self):
89        self._touch('__init__.py', self.pkg_dir)
90        packages = find_packages(self.dist_dir)
91        assert packages == ['pkg', 'pkg.subpkg']
92
93    def test_exclude(self):
94        self._touch('__init__.py', self.pkg_dir)
95        packages = find_packages(self.dist_dir, exclude=('pkg.*',))
96        assert packages == ['pkg']
97
98    def test_exclude_recursive(self):
99        """
100        Excluding a parent package should not exclude child packages as well.
101        """
102        self._touch('__init__.py', self.pkg_dir)
103        self._touch('__init__.py', self.sub_pkg_dir)
104        packages = find_packages(self.dist_dir, exclude=('pkg',))
105        assert packages == ['pkg.subpkg']
106
107    def test_include_excludes_other(self):
108        """
109        If include is specified, other packages should be excluded.
110        """
111        self._touch('__init__.py', self.pkg_dir)
112        alt_dir = self._mkdir('other_pkg', self.dist_dir)
113        self._touch('__init__.py', alt_dir)
114        packages = find_packages(self.dist_dir, include=['other_pkg'])
115        assert packages == ['other_pkg']
116
117    def test_dir_with_dot_is_skipped(self):
118        shutil.rmtree(os.path.join(self.dist_dir, 'pkg/subpkg/assets'))
119        data_dir = self._mkdir('some.data', self.pkg_dir)
120        self._touch('__init__.py', data_dir)
121        self._touch('file.dat', data_dir)
122        packages = find_packages(self.dist_dir)
123        assert 'pkg.some.data' not in packages
124
125    def test_dir_with_packages_in_subdir_is_excluded(self):
126        """
127        Ensure that a package in a non-package such as build/pkg/__init__.py
128        is excluded.
129        """
130        build_dir = self._mkdir('build', self.dist_dir)
131        build_pkg_dir = self._mkdir('pkg', build_dir)
132        self._touch('__init__.py', build_pkg_dir)
133        packages = find_packages(self.dist_dir)
134        assert 'build.pkg' not in packages
135
136    @pytest.mark.skipif(not has_symlink(), reason='Symlink support required')
137    def test_symlinked_packages_are_included(self):
138        """
139        A symbolically-linked directory should be treated like any other
140        directory when matched as a package.
141
142        Create a link from lpkg -> pkg.
143        """
144        self._touch('__init__.py', self.pkg_dir)
145        linked_pkg = os.path.join(self.dist_dir, 'lpkg')
146        os.symlink('pkg', linked_pkg)
147        assert os.path.isdir(linked_pkg)
148        packages = find_packages(self.dist_dir)
149        assert 'lpkg' in packages
150
151    def _assert_packages(self, actual, expected):
152        assert set(actual) == set(expected)
153
154    def test_pep420_ns_package(self):
155        packages = find_namespace_packages(
156            self.dist_dir, include=['pkg*'], exclude=['pkg.subpkg.assets'])
157        self._assert_packages(packages, ['pkg', 'pkg.nspkg', 'pkg.subpkg'])
158
159    def test_pep420_ns_package_no_includes(self):
160        packages = find_namespace_packages(
161            self.dist_dir, exclude=['pkg.subpkg.assets'])
162        self._assert_packages(
163            packages, ['docs', 'pkg', 'pkg.nspkg', 'pkg.subpkg'])
164
165    def test_pep420_ns_package_no_includes_or_excludes(self):
166        packages = find_namespace_packages(self.dist_dir)
167        expected = [
168            'docs', 'pkg', 'pkg.nspkg', 'pkg.subpkg', 'pkg.subpkg.assets']
169        self._assert_packages(packages, expected)
170
171    def test_regular_package_with_nested_pep420_ns_packages(self):
172        self._touch('__init__.py', self.pkg_dir)
173        packages = find_namespace_packages(
174            self.dist_dir, exclude=['docs', 'pkg.subpkg.assets'])
175        self._assert_packages(packages, ['pkg', 'pkg.nspkg', 'pkg.subpkg'])
176
177    def test_pep420_ns_package_no_non_package_dirs(self):
178        shutil.rmtree(self.docs_dir)
179        shutil.rmtree(os.path.join(self.dist_dir, 'pkg/subpkg/assets'))
180        packages = find_namespace_packages(self.dist_dir)
181        self._assert_packages(packages, ['pkg', 'pkg.nspkg', 'pkg.subpkg'])
182
183
184class TestFlatLayoutPackageFinder:
185    EXAMPLES = {
186        "hidden-folders": (
187            [".pkg/__init__.py", "pkg/__init__.py", "pkg/nested/file.txt"],
188            ["pkg", "pkg.nested"]
189        ),
190        "private-packages": (
191            ["_pkg/__init__.py", "pkg/_private/__init__.py"],
192            ["pkg", "pkg._private"]
193        ),
194        "invalid-name": (
195            ["invalid-pkg/__init__.py", "other.pkg/__init__.py", "yet,another/file.py"],
196            []
197        ),
198        "docs": (
199            ["pkg/__init__.py", "docs/conf.py", "docs/readme.rst"],
200            ["pkg"]
201        ),
202        "tests": (
203            ["pkg/__init__.py", "tests/test_pkg.py", "tests/__init__.py"],
204            ["pkg"]
205        ),
206        "examples": (
207            [
208                "pkg/__init__.py",
209                "examples/__init__.py",
210                "examples/file.py"
211                "example/other_file.py",
212                # Sub-packages should always be fine
213                "pkg/example/__init__.py",
214                "pkg/examples/__init__.py",
215            ],
216            ["pkg", "pkg.examples", "pkg.example"]
217        ),
218        "tool-specific": (
219            [
220                "pkg/__init__.py",
221                "tasks/__init__.py",
222                "tasks/subpackage/__init__.py",
223                "fabfile/__init__.py",
224                "fabfile/subpackage/__init__.py",
225                # Sub-packages should always be fine
226                "pkg/tasks/__init__.py",
227                "pkg/fabfile/__init__.py",
228            ],
229            ["pkg", "pkg.tasks", "pkg.fabfile"]
230        )
231    }
232
233    @pytest.mark.parametrize("example", EXAMPLES.keys())
234    def test_unwanted_directories_not_included(self, tmp_path, example):
235        files, expected_packages = self.EXAMPLES[example]
236        ensure_files(tmp_path, files)
237        found_packages = FlatLayoutPackageFinder.find(str(tmp_path))
238        assert set(found_packages) == set(expected_packages)
239
240
241def ensure_files(root_path, files):
242    for file in files:
243        path = root_path / file
244        path.parent.mkdir(parents=True, exist_ok=True)
245        path.touch()
246