243 lines
7.3 KiB
Python
243 lines
7.3 KiB
Python
|
"""Extensions to the 'distutils' for large or complex distributions"""
|
||
|
|
||
|
from fnmatch import fnmatchcase
|
||
|
import functools
|
||
|
import os
|
||
|
import re
|
||
|
|
||
|
import _distutils_hack.override # noqa: F401
|
||
|
|
||
|
import distutils.core
|
||
|
from distutils.errors import DistutilsOptionError
|
||
|
from distutils.util import convert_path
|
||
|
|
||
|
from ._deprecation_warning import SetuptoolsDeprecationWarning
|
||
|
|
||
|
import setuptools.version
|
||
|
from setuptools.extension import Extension
|
||
|
from setuptools.dist import Distribution
|
||
|
from setuptools.depends import Require
|
||
|
from . import monkey
|
||
|
|
||
|
|
||
|
__all__ = [
|
||
|
'setup',
|
||
|
'Distribution',
|
||
|
'Command',
|
||
|
'Extension',
|
||
|
'Require',
|
||
|
'SetuptoolsDeprecationWarning',
|
||
|
'find_packages',
|
||
|
'find_namespace_packages',
|
||
|
]
|
||
|
|
||
|
__version__ = setuptools.version.__version__
|
||
|
|
||
|
bootstrap_install_from = None
|
||
|
|
||
|
|
||
|
class PackageFinder:
|
||
|
"""
|
||
|
Generate a list of all Python packages found within a directory
|
||
|
"""
|
||
|
|
||
|
@classmethod
|
||
|
def find(cls, where='.', exclude=(), include=('*',)):
|
||
|
"""Return a list all Python packages found within directory 'where'
|
||
|
|
||
|
'where' is the root directory which will be searched for packages. It
|
||
|
should be supplied as a "cross-platform" (i.e. URL-style) path; it will
|
||
|
be converted to the appropriate local path syntax.
|
||
|
|
||
|
'exclude' is a sequence of package names to exclude; '*' can be used
|
||
|
as a wildcard in the names, such that 'foo.*' will exclude all
|
||
|
subpackages of 'foo' (but not 'foo' itself).
|
||
|
|
||
|
'include' is a sequence of package names to include. If it's
|
||
|
specified, only the named packages will be included. If it's not
|
||
|
specified, all found packages will be included. 'include' can contain
|
||
|
shell style wildcard patterns just like 'exclude'.
|
||
|
"""
|
||
|
|
||
|
return list(
|
||
|
cls._find_packages_iter(
|
||
|
convert_path(where),
|
||
|
cls._build_filter('ez_setup', '*__pycache__', *exclude),
|
||
|
cls._build_filter(*include),
|
||
|
)
|
||
|
)
|
||
|
|
||
|
@classmethod
|
||
|
def _find_packages_iter(cls, where, exclude, include):
|
||
|
"""
|
||
|
All the packages found in 'where' that pass the 'include' filter, but
|
||
|
not the 'exclude' filter.
|
||
|
"""
|
||
|
for root, dirs, files in os.walk(where, followlinks=True):
|
||
|
# Copy dirs to iterate over it, then empty dirs.
|
||
|
all_dirs = dirs[:]
|
||
|
dirs[:] = []
|
||
|
|
||
|
for dir in all_dirs:
|
||
|
full_path = os.path.join(root, dir)
|
||
|
rel_path = os.path.relpath(full_path, where)
|
||
|
package = rel_path.replace(os.path.sep, '.')
|
||
|
|
||
|
# Skip directory trees that are not valid packages
|
||
|
if '.' in dir or not cls._looks_like_package(full_path):
|
||
|
continue
|
||
|
|
||
|
# Should this package be included?
|
||
|
if include(package) and not exclude(package):
|
||
|
yield package
|
||
|
|
||
|
# Keep searching subdirectories, as there may be more packages
|
||
|
# down there, even if the parent was excluded.
|
||
|
dirs.append(dir)
|
||
|
|
||
|
@staticmethod
|
||
|
def _looks_like_package(path):
|
||
|
"""Does a directory look like a package?"""
|
||
|
return os.path.isfile(os.path.join(path, '__init__.py'))
|
||
|
|
||
|
@staticmethod
|
||
|
def _build_filter(*patterns):
|
||
|
"""
|
||
|
Given a list of patterns, return a callable that will be true only if
|
||
|
the input matches at least one of the patterns.
|
||
|
"""
|
||
|
return lambda name: any(fnmatchcase(name, pat=pat) for pat in patterns)
|
||
|
|
||
|
|
||
|
class PEP420PackageFinder(PackageFinder):
|
||
|
@staticmethod
|
||
|
def _looks_like_package(path):
|
||
|
return True
|
||
|
|
||
|
|
||
|
find_packages = PackageFinder.find
|
||
|
find_namespace_packages = PEP420PackageFinder.find
|
||
|
|
||
|
|
||
|
def _install_setup_requires(attrs):
|
||
|
# Note: do not use `setuptools.Distribution` directly, as
|
||
|
# our PEP 517 backend patch `distutils.core.Distribution`.
|
||
|
class MinimalDistribution(distutils.core.Distribution):
|
||
|
"""
|
||
|
A minimal version of a distribution for supporting the
|
||
|
fetch_build_eggs interface.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, attrs):
|
||
|
_incl = 'dependency_links', 'setup_requires'
|
||
|
filtered = {k: attrs[k] for k in set(_incl) & set(attrs)}
|
||
|
distutils.core.Distribution.__init__(self, filtered)
|
||
|
|
||
|
def finalize_options(self):
|
||
|
"""
|
||
|
Disable finalize_options to avoid building the working set.
|
||
|
Ref #2158.
|
||
|
"""
|
||
|
|
||
|
dist = MinimalDistribution(attrs)
|
||
|
|
||
|
# Honor setup.cfg's options.
|
||
|
dist.parse_config_files(ignore_option_errors=True)
|
||
|
if dist.setup_requires:
|
||
|
dist.fetch_build_eggs(dist.setup_requires)
|
||
|
|
||
|
|
||
|
def setup(**attrs):
|
||
|
# Make sure we have any requirements needed to interpret 'attrs'.
|
||
|
_install_setup_requires(attrs)
|
||
|
return distutils.core.setup(**attrs)
|
||
|
|
||
|
|
||
|
setup.__doc__ = distutils.core.setup.__doc__
|
||
|
|
||
|
|
||
|
_Command = monkey.get_unpatched(distutils.core.Command)
|
||
|
|
||
|
|
||
|
class Command(_Command):
|
||
|
__doc__ = _Command.__doc__
|
||
|
|
||
|
command_consumes_arguments = False
|
||
|
|
||
|
def __init__(self, dist, **kw):
|
||
|
"""
|
||
|
Construct the command for dist, updating
|
||
|
vars(self) with any keyword parameters.
|
||
|
"""
|
||
|
_Command.__init__(self, dist)
|
||
|
vars(self).update(kw)
|
||
|
|
||
|
def _ensure_stringlike(self, option, what, default=None):
|
||
|
val = getattr(self, option)
|
||
|
if val is None:
|
||
|
setattr(self, option, default)
|
||
|
return default
|
||
|
elif not isinstance(val, str):
|
||
|
raise DistutilsOptionError(
|
||
|
"'%s' must be a %s (got `%s`)" % (option, what, val)
|
||
|
)
|
||
|
return val
|
||
|
|
||
|
def ensure_string_list(self, option):
|
||
|
r"""Ensure that 'option' is a list of strings. If 'option' is
|
||
|
currently a string, we split it either on /,\s*/ or /\s+/, so
|
||
|
"foo bar baz", "foo,bar,baz", and "foo, bar baz" all become
|
||
|
["foo", "bar", "baz"].
|
||
|
"""
|
||
|
val = getattr(self, option)
|
||
|
if val is None:
|
||
|
return
|
||
|
elif isinstance(val, str):
|
||
|
setattr(self, option, re.split(r',\s*|\s+', val))
|
||
|
else:
|
||
|
if isinstance(val, list):
|
||
|
ok = all(isinstance(v, str) for v in val)
|
||
|
else:
|
||
|
ok = False
|
||
|
if not ok:
|
||
|
raise DistutilsOptionError(
|
||
|
"'%s' must be a list of strings (got %r)" % (option, val)
|
||
|
)
|
||
|
|
||
|
def reinitialize_command(self, command, reinit_subcommands=0, **kw):
|
||
|
cmd = _Command.reinitialize_command(self, command, reinit_subcommands)
|
||
|
vars(cmd).update(kw)
|
||
|
return cmd
|
||
|
|
||
|
|
||
|
def _find_all_simple(path):
|
||
|
"""
|
||
|
Find all files under 'path'
|
||
|
"""
|
||
|
results = (
|
||
|
os.path.join(base, file)
|
||
|
for base, dirs, files in os.walk(path, followlinks=True)
|
||
|
for file in files
|
||
|
)
|
||
|
return filter(os.path.isfile, results)
|
||
|
|
||
|
|
||
|
def findall(dir=os.curdir):
|
||
|
"""
|
||
|
Find all files under 'dir' and return the list of full filenames.
|
||
|
Unless dir is '.', return full filenames with dir prepended.
|
||
|
"""
|
||
|
files = _find_all_simple(dir)
|
||
|
if dir == os.curdir:
|
||
|
make_rel = functools.partial(os.path.relpath, start=dir)
|
||
|
files = map(make_rel, files)
|
||
|
return list(files)
|
||
|
|
||
|
|
||
|
class sic(str):
|
||
|
"""Treat this string as-is (https://en.wikipedia.org/wiki/Sic)"""
|
||
|
|
||
|
|
||
|
# Apply monkey patches
|
||
|
monkey.patch_all()
|