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()
229