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