build_py.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. from glob import glob
  2. from distutils.util import convert_path
  3. import distutils.command.build_py as orig
  4. import os
  5. import fnmatch
  6. import textwrap
  7. import io
  8. import distutils.errors
  9. import itertools
  10. from setuptools.extern import six
  11. from setuptools.extern.six.moves import map, filter, filterfalse
  12. try:
  13. from setuptools.lib2to3_ex import Mixin2to3
  14. except ImportError:
  15. class Mixin2to3:
  16. def run_2to3(self, files, doctests=True):
  17. "do nothing"
  18. class build_py(orig.build_py, Mixin2to3):
  19. """Enhanced 'build_py' command that includes data files with packages
  20. The data files are specified via a 'package_data' argument to 'setup()'.
  21. See 'setuptools.dist.Distribution' for more details.
  22. Also, this version of the 'build_py' command allows you to specify both
  23. 'py_modules' and 'packages' in the same setup operation.
  24. """
  25. def finalize_options(self):
  26. orig.build_py.finalize_options(self)
  27. self.package_data = self.distribution.package_data
  28. self.exclude_package_data = (self.distribution.exclude_package_data or
  29. {})
  30. if 'data_files' in self.__dict__:
  31. del self.__dict__['data_files']
  32. self.__updated_files = []
  33. self.__doctests_2to3 = []
  34. def run(self):
  35. """Build modules, packages, and copy data files to build directory"""
  36. if not self.py_modules and not self.packages:
  37. return
  38. if self.py_modules:
  39. self.build_modules()
  40. if self.packages:
  41. self.build_packages()
  42. self.build_package_data()
  43. self.run_2to3(self.__updated_files, False)
  44. self.run_2to3(self.__updated_files, True)
  45. self.run_2to3(self.__doctests_2to3, True)
  46. # Only compile actual .py files, using our base class' idea of what our
  47. # output files are.
  48. self.byte_compile(orig.build_py.get_outputs(self, include_bytecode=0))
  49. def __getattr__(self, attr):
  50. "lazily compute data files"
  51. if attr == 'data_files':
  52. self.data_files = self._get_data_files()
  53. return self.data_files
  54. return orig.build_py.__getattr__(self, attr)
  55. def build_module(self, module, module_file, package):
  56. if six.PY2 and isinstance(package, six.string_types):
  57. # avoid errors on Python 2 when unicode is passed (#190)
  58. package = package.split('.')
  59. outfile, copied = orig.build_py.build_module(self, module, module_file,
  60. package)
  61. if copied:
  62. self.__updated_files.append(outfile)
  63. return outfile, copied
  64. def _get_data_files(self):
  65. """Generate list of '(package,src_dir,build_dir,filenames)' tuples"""
  66. self.analyze_manifest()
  67. return list(map(self._get_pkg_data_files, self.packages or ()))
  68. def _get_pkg_data_files(self, package):
  69. # Locate package source directory
  70. src_dir = self.get_package_dir(package)
  71. # Compute package build directory
  72. build_dir = os.path.join(*([self.build_lib] + package.split('.')))
  73. # Strip directory from globbed filenames
  74. filenames = [
  75. os.path.relpath(file, src_dir)
  76. for file in self.find_data_files(package, src_dir)
  77. ]
  78. return package, src_dir, build_dir, filenames
  79. def find_data_files(self, package, src_dir):
  80. """Return filenames for package's data files in 'src_dir'"""
  81. patterns = self._get_platform_patterns(
  82. self.package_data,
  83. package,
  84. src_dir,
  85. )
  86. globs_expanded = map(glob, patterns)
  87. # flatten the expanded globs into an iterable of matches
  88. globs_matches = itertools.chain.from_iterable(globs_expanded)
  89. glob_files = filter(os.path.isfile, globs_matches)
  90. files = itertools.chain(
  91. self.manifest_files.get(package, []),
  92. glob_files,
  93. )
  94. return self.exclude_data_files(package, src_dir, files)
  95. def build_package_data(self):
  96. """Copy data files into build directory"""
  97. for package, src_dir, build_dir, filenames in self.data_files:
  98. for filename in filenames:
  99. target = os.path.join(build_dir, filename)
  100. self.mkpath(os.path.dirname(target))
  101. srcfile = os.path.join(src_dir, filename)
  102. outf, copied = self.copy_file(srcfile, target)
  103. srcfile = os.path.abspath(srcfile)
  104. if (copied and
  105. srcfile in self.distribution.convert_2to3_doctests):
  106. self.__doctests_2to3.append(outf)
  107. def analyze_manifest(self):
  108. self.manifest_files = mf = {}
  109. if not self.distribution.include_package_data:
  110. return
  111. src_dirs = {}
  112. for package in self.packages or ():
  113. # Locate package source directory
  114. src_dirs[assert_relative(self.get_package_dir(package))] = package
  115. self.run_command('egg_info')
  116. ei_cmd = self.get_finalized_command('egg_info')
  117. for path in ei_cmd.filelist.files:
  118. d, f = os.path.split(assert_relative(path))
  119. prev = None
  120. oldf = f
  121. while d and d != prev and d not in src_dirs:
  122. prev = d
  123. d, df = os.path.split(d)
  124. f = os.path.join(df, f)
  125. if d in src_dirs:
  126. if path.endswith('.py') and f == oldf:
  127. continue # it's a module, not data
  128. mf.setdefault(src_dirs[d], []).append(path)
  129. def get_data_files(self):
  130. pass # Lazily compute data files in _get_data_files() function.
  131. def check_package(self, package, package_dir):
  132. """Check namespace packages' __init__ for declare_namespace"""
  133. try:
  134. return self.packages_checked[package]
  135. except KeyError:
  136. pass
  137. init_py = orig.build_py.check_package(self, package, package_dir)
  138. self.packages_checked[package] = init_py
  139. if not init_py or not self.distribution.namespace_packages:
  140. return init_py
  141. for pkg in self.distribution.namespace_packages:
  142. if pkg == package or pkg.startswith(package + '.'):
  143. break
  144. else:
  145. return init_py
  146. with io.open(init_py, 'rb') as f:
  147. contents = f.read()
  148. if b'declare_namespace' not in contents:
  149. raise distutils.errors.DistutilsError(
  150. "Namespace package problem: %s is a namespace package, but "
  151. "its\n__init__.py does not call declare_namespace()! Please "
  152. 'fix it.\n(See the setuptools manual under '
  153. '"Namespace Packages" for details.)\n"' % (package,)
  154. )
  155. return init_py
  156. def initialize_options(self):
  157. self.packages_checked = {}
  158. orig.build_py.initialize_options(self)
  159. def get_package_dir(self, package):
  160. res = orig.build_py.get_package_dir(self, package)
  161. if self.distribution.src_root is not None:
  162. return os.path.join(self.distribution.src_root, res)
  163. return res
  164. def exclude_data_files(self, package, src_dir, files):
  165. """Filter filenames for package's data files in 'src_dir'"""
  166. files = list(files)
  167. patterns = self._get_platform_patterns(
  168. self.exclude_package_data,
  169. package,
  170. src_dir,
  171. )
  172. match_groups = (
  173. fnmatch.filter(files, pattern)
  174. for pattern in patterns
  175. )
  176. # flatten the groups of matches into an iterable of matches
  177. matches = itertools.chain.from_iterable(match_groups)
  178. bad = set(matches)
  179. keepers = (
  180. fn
  181. for fn in files
  182. if fn not in bad
  183. )
  184. # ditch dupes
  185. return list(_unique_everseen(keepers))
  186. @staticmethod
  187. def _get_platform_patterns(spec, package, src_dir):
  188. """
  189. yield platform-specific path patterns (suitable for glob
  190. or fn_match) from a glob-based spec (such as
  191. self.package_data or self.exclude_package_data)
  192. matching package in src_dir.
  193. """
  194. raw_patterns = itertools.chain(
  195. spec.get('', []),
  196. spec.get(package, []),
  197. )
  198. return (
  199. # Each pattern has to be converted to a platform-specific path
  200. os.path.join(src_dir, convert_path(pattern))
  201. for pattern in raw_patterns
  202. )
  203. # from Python docs
  204. def _unique_everseen(iterable, key=None):
  205. "List unique elements, preserving order. Remember all elements ever seen."
  206. # unique_everseen('AAAABBBCCDAABBB') --> A B C D
  207. # unique_everseen('ABBCcAD', str.lower) --> A B C D
  208. seen = set()
  209. seen_add = seen.add
  210. if key is None:
  211. for element in filterfalse(seen.__contains__, iterable):
  212. seen_add(element)
  213. yield element
  214. else:
  215. for element in iterable:
  216. k = key(element)
  217. if k not in seen:
  218. seen_add(k)
  219. yield element
  220. def assert_relative(path):
  221. if not os.path.isabs(path):
  222. return path
  223. from distutils.errors import DistutilsSetupError
  224. msg = textwrap.dedent("""
  225. Error: setup script specifies an absolute path:
  226. %s
  227. setup() arguments must *always* be /-separated paths relative to the
  228. setup.py directory, *never* absolute paths.
  229. """).lstrip() % path
  230. raise DistutilsSetupError(msg)