1"""Extensions to the 'distutils' for large or complex distributions""" 2 3from fnmatch import fnmatchcase 4import functools 5import os 6import re 7 8import _distutils_hack.override # noqa: F401 9 10import distutils.core 11from distutils.errors import DistutilsOptionError 12from distutils.util import convert_path 13 14from ._deprecation_warning import SetuptoolsDeprecationWarning 15 16import setuptools.version 17from setuptools.extension import Extension 18from setuptools.dist import Distribution 19from setuptools.depends import Require 20from . import monkey 21 22 23__all__ = [ 24 'setup', 'Distribution', 'Command', 'Extension', 'Require', 25 'SetuptoolsDeprecationWarning', 26 'find_packages', 'find_namespace_packages', 27] 28 29__version__ = setuptools.version.__version__ 30 31bootstrap_install_from = None 32 33# If we run 2to3 on .py files, should we also convert docstrings? 34# Default: yes; assume that we can detect doctests reliably 35run_2to3_on_doctests = True 36# Standard package names for fixer packages 37lib2to3_fixer_packages = ['lib2to3.fixes'] 38 39 40class PackageFinder: 41 """ 42 Generate a list of all Python packages found within a directory 43 """ 44 45 @classmethod 46 def find(cls, where='.', exclude=(), include=('*',)): 47 """Return a list all Python packages found within directory 'where' 48 49 'where' is the root directory which will be searched for packages. It 50 should be supplied as a "cross-platform" (i.e. URL-style) path; it will 51 be converted to the appropriate local path syntax. 52 53 'exclude' is a sequence of package names to exclude; '*' can be used 54 as a wildcard in the names, such that 'foo.*' will exclude all 55 subpackages of 'foo' (but not 'foo' itself). 56 57 'include' is a sequence of package names to include. If it's 58 specified, only the named packages will be included. If it's not 59 specified, all found packages will be included. 'include' can contain 60 shell style wildcard patterns just like 'exclude'. 61 """ 62 63 return list(cls._find_packages_iter( 64 convert_path(where), 65 cls._build_filter('ez_setup', '*__pycache__', *exclude), 66 cls._build_filter(*include))) 67 68 @classmethod 69 def _find_packages_iter(cls, where, exclude, include): 70 """ 71 All the packages found in 'where' that pass the 'include' filter, but 72 not the 'exclude' filter. 73 """ 74 for root, dirs, files in os.walk(where, followlinks=True): 75 # Copy dirs to iterate over it, then empty dirs. 76 all_dirs = dirs[:] 77 dirs[:] = [] 78 79 for dir in all_dirs: 80 full_path = os.path.join(root, dir) 81 rel_path = os.path.relpath(full_path, where) 82 package = rel_path.replace(os.path.sep, '.') 83 84 # Skip directory trees that are not valid packages 85 if ('.' in dir or not cls._looks_like_package(full_path)): 86 continue 87 88 # Should this package be included? 89 if include(package) and not exclude(package): 90 yield package 91 92 # Keep searching subdirectories, as there may be more packages 93 # down there, even if the parent was excluded. 94 dirs.append(dir) 95 96 @staticmethod 97 def _looks_like_package(path): 98 """Does a directory look like a package?""" 99 return os.path.isfile(os.path.join(path, '__init__.py')) 100 101 @staticmethod 102 def _build_filter(*patterns): 103 """ 104 Given a list of patterns, return a callable that will be true only if 105 the input matches at least one of the patterns. 106 """ 107 return lambda name: any(fnmatchcase(name, pat=pat) for pat in patterns) 108 109 110class PEP420PackageFinder(PackageFinder): 111 @staticmethod 112 def _looks_like_package(path): 113 return True 114 115 116find_packages = PackageFinder.find 117find_namespace_packages = PEP420PackageFinder.find 118 119 120def _install_setup_requires(attrs): 121 # Note: do not use `setuptools.Distribution` directly, as 122 # our PEP 517 backend patch `distutils.core.Distribution`. 123 class MinimalDistribution(distutils.core.Distribution): 124 """ 125 A minimal version of a distribution for supporting the 126 fetch_build_eggs interface. 127 """ 128 def __init__(self, attrs): 129 _incl = 'dependency_links', 'setup_requires' 130 filtered = { 131 k: attrs[k] 132 for k in set(_incl) & set(attrs) 133 } 134 distutils.core.Distribution.__init__(self, filtered) 135 136 def finalize_options(self): 137 """ 138 Disable finalize_options to avoid building the working set. 139 Ref #2158. 140 """ 141 142 dist = MinimalDistribution(attrs) 143 144 # Honor setup.cfg's options. 145 dist.parse_config_files(ignore_option_errors=True) 146 if dist.setup_requires: 147 dist.fetch_build_eggs(dist.setup_requires) 148 149 150def setup(**attrs): 151 # Make sure we have any requirements needed to interpret 'attrs'. 152 _install_setup_requires(attrs) 153 return distutils.core.setup(**attrs) 154 155 156setup.__doc__ = distutils.core.setup.__doc__ 157 158 159_Command = monkey.get_unpatched(distutils.core.Command) 160 161 162class Command(_Command): 163 __doc__ = _Command.__doc__ 164 165 command_consumes_arguments = False 166 167 def __init__(self, dist, **kw): 168 """ 169 Construct the command for dist, updating 170 vars(self) with any keyword parameters. 171 """ 172 _Command.__init__(self, dist) 173 vars(self).update(kw) 174 175 def _ensure_stringlike(self, option, what, default=None): 176 val = getattr(self, option) 177 if val is None: 178 setattr(self, option, default) 179 return default 180 elif not isinstance(val, str): 181 raise DistutilsOptionError("'%s' must be a %s (got `%s`)" 182 % (option, what, val)) 183 return val 184 185 def ensure_string_list(self, option): 186 r"""Ensure that 'option' is a list of strings. If 'option' is 187 currently a string, we split it either on /,\s*/ or /\s+/, so 188 "foo bar baz", "foo,bar,baz", and "foo, bar baz" all become 189 ["foo", "bar", "baz"]. 190 """ 191 val = getattr(self, option) 192 if val is None: 193 return 194 elif isinstance(val, str): 195 setattr(self, option, re.split(r',\s*|\s+', val)) 196 else: 197 if isinstance(val, list): 198 ok = all(isinstance(v, str) for v in val) 199 else: 200 ok = False 201 if not ok: 202 raise DistutilsOptionError( 203 "'%s' must be a list of strings (got %r)" 204 % (option, val)) 205 206 def reinitialize_command(self, command, reinit_subcommands=0, **kw): 207 cmd = _Command.reinitialize_command(self, command, reinit_subcommands) 208 vars(cmd).update(kw) 209 return cmd 210 211 212def _find_all_simple(path): 213 """ 214 Find all files under 'path' 215 """ 216 results = ( 217 os.path.join(base, file) 218 for base, dirs, files in os.walk(path, followlinks=True) 219 for file in files 220 ) 221 return filter(os.path.isfile, results) 222 223 224def findall(dir=os.curdir): 225 """ 226 Find all files under 'dir' and return the list of full filenames. 227 Unless dir is '.', return full filenames with dir prepended. 228 """ 229 files = _find_all_simple(dir) 230 if dir == os.curdir: 231 make_rel = functools.partial(os.path.relpath, start=dir) 232 files = map(make_rel, files) 233 return list(files) 234 235 236class sic(str): 237 """Treat this string as-is (https://en.wikipedia.org/wiki/Sic)""" 238 239 240# Apply monkey patches 241monkey.patch_all() 242