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