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