build_py.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. import os
  2. import sys
  3. import fnmatch
  4. import textwrap
  5. import distutils.command.build_py as orig
  6. from distutils.util import convert_path
  7. from glob import glob
  8. try:
  9. from setuptools.lib2to3_ex import Mixin2to3
  10. except ImportError:
  11. class Mixin2to3:
  12. def run_2to3(self, files, doctests=True):
  13. "do nothing"
  14. class build_py(orig.build_py, Mixin2to3):
  15. """Enhanced 'build_py' command that includes data files with packages
  16. The data files are specified via a 'package_data' argument to 'setup()'.
  17. See 'setuptools.dist.Distribution' for more details.
  18. Also, this version of the 'build_py' command allows you to specify both
  19. 'py_modules' and 'packages' in the same setup operation.
  20. """
  21. def finalize_options(self):
  22. orig.build_py.finalize_options(self)
  23. self.package_data = self.distribution.package_data
  24. self.exclude_package_data = self.distribution.exclude_package_data or {}
  25. if 'data_files' in self.__dict__: del self.__dict__['data_files']
  26. self.__updated_files = []
  27. self.__doctests_2to3 = []
  28. def run(self):
  29. """Build modules, packages, and copy data files to build directory"""
  30. if not self.py_modules and not self.packages:
  31. return
  32. if self.py_modules:
  33. self.build_modules()
  34. if self.packages:
  35. self.build_packages()
  36. self.build_package_data()
  37. self.run_2to3(self.__updated_files, False)
  38. self.run_2to3(self.__updated_files, True)
  39. self.run_2to3(self.__doctests_2to3, True)
  40. # Only compile actual .py files, using our base class' idea of what our
  41. # output files are.
  42. self.byte_compile(orig.build_py.get_outputs(self, include_bytecode=0))
  43. def __getattr__(self, attr):
  44. if attr=='data_files': # lazily compute data files
  45. self.data_files = files = self._get_data_files()
  46. return files
  47. return orig.build_py.__getattr__(self,attr)
  48. def build_module(self, module, module_file, package):
  49. outfile, copied = orig.build_py.build_module(self, module, module_file, package)
  50. if copied:
  51. self.__updated_files.append(outfile)
  52. return outfile, copied
  53. def _get_data_files(self):
  54. """Generate list of '(package,src_dir,build_dir,filenames)' tuples"""
  55. self.analyze_manifest()
  56. data = []
  57. for package in self.packages or ():
  58. # Locate package source directory
  59. src_dir = self.get_package_dir(package)
  60. # Compute package build directory
  61. build_dir = os.path.join(*([self.build_lib] + package.split('.')))
  62. # Length of path to strip from found files
  63. plen = len(src_dir)+1
  64. # Strip directory from globbed filenames
  65. filenames = [
  66. file[plen:] for file in self.find_data_files(package, src_dir)
  67. ]
  68. data.append((package, src_dir, build_dir, filenames))
  69. return data
  70. def find_data_files(self, package, src_dir):
  71. """Return filenames for package's data files in 'src_dir'"""
  72. globs = (self.package_data.get('', [])
  73. + self.package_data.get(package, []))
  74. files = self.manifest_files.get(package, [])[:]
  75. for pattern in globs:
  76. # Each pattern has to be converted to a platform-specific path
  77. files.extend(glob(os.path.join(src_dir, convert_path(pattern))))
  78. return self.exclude_data_files(package, src_dir, files)
  79. def build_package_data(self):
  80. """Copy data files into build directory"""
  81. for package, src_dir, build_dir, filenames in self.data_files:
  82. for filename in filenames:
  83. target = os.path.join(build_dir, filename)
  84. self.mkpath(os.path.dirname(target))
  85. srcfile = os.path.join(src_dir, filename)
  86. outf, copied = self.copy_file(srcfile, target)
  87. srcfile = os.path.abspath(srcfile)
  88. if copied and srcfile in self.distribution.convert_2to3_doctests:
  89. self.__doctests_2to3.append(outf)
  90. def analyze_manifest(self):
  91. self.manifest_files = mf = {}
  92. if not self.distribution.include_package_data:
  93. return
  94. src_dirs = {}
  95. for package in self.packages or ():
  96. # Locate package source directory
  97. src_dirs[assert_relative(self.get_package_dir(package))] = package
  98. self.run_command('egg_info')
  99. ei_cmd = self.get_finalized_command('egg_info')
  100. for path in ei_cmd.filelist.files:
  101. d,f = os.path.split(assert_relative(path))
  102. prev = None
  103. oldf = f
  104. while d and d!=prev and d not in src_dirs:
  105. prev = d
  106. d, df = os.path.split(d)
  107. f = os.path.join(df, f)
  108. if d in src_dirs:
  109. if path.endswith('.py') and f==oldf:
  110. continue # it's a module, not data
  111. mf.setdefault(src_dirs[d],[]).append(path)
  112. def get_data_files(self): pass # kludge 2.4 for lazy computation
  113. if sys.version<"2.4": # Python 2.4 already has this code
  114. def get_outputs(self, include_bytecode=1):
  115. """Return complete list of files copied to the build directory
  116. This includes both '.py' files and data files, as well as '.pyc'
  117. and '.pyo' files if 'include_bytecode' is true. (This method is
  118. needed for the 'install_lib' command to do its job properly, and to
  119. generate a correct installation manifest.)
  120. """
  121. return orig.build_py.get_outputs(self, include_bytecode) + [
  122. os.path.join(build_dir, filename)
  123. for package, src_dir, build_dir,filenames in self.data_files
  124. for filename in filenames
  125. ]
  126. def check_package(self, package, package_dir):
  127. """Check namespace packages' __init__ for declare_namespace"""
  128. try:
  129. return self.packages_checked[package]
  130. except KeyError:
  131. pass
  132. init_py = orig.build_py.check_package(self, package, package_dir)
  133. self.packages_checked[package] = init_py
  134. if not init_py or not self.distribution.namespace_packages:
  135. return init_py
  136. for pkg in self.distribution.namespace_packages:
  137. if pkg==package or pkg.startswith(package+'.'):
  138. break
  139. else:
  140. return init_py
  141. f = open(init_py,'rbU')
  142. if 'declare_namespace'.encode() not in f.read():
  143. from distutils.errors import DistutilsError
  144. raise DistutilsError(
  145. "Namespace package problem: %s is a namespace package, but its\n"
  146. "__init__.py does not call declare_namespace()! Please fix it.\n"
  147. '(See the setuptools manual under "Namespace Packages" for '
  148. "details.)\n" % (package,)
  149. )
  150. f.close()
  151. return init_py
  152. def initialize_options(self):
  153. self.packages_checked={}
  154. orig.build_py.initialize_options(self)
  155. def get_package_dir(self, package):
  156. res = orig.build_py.get_package_dir(self, package)
  157. if self.distribution.src_root is not None:
  158. return os.path.join(self.distribution.src_root, res)
  159. return res
  160. def exclude_data_files(self, package, src_dir, files):
  161. """Filter filenames for package's data files in 'src_dir'"""
  162. globs = (self.exclude_package_data.get('', [])
  163. + self.exclude_package_data.get(package, []))
  164. bad = []
  165. for pattern in globs:
  166. bad.extend(
  167. fnmatch.filter(
  168. files, os.path.join(src_dir, convert_path(pattern))
  169. )
  170. )
  171. bad = dict.fromkeys(bad)
  172. seen = {}
  173. return [
  174. f for f in files if f not in bad
  175. and f not in seen and seen.setdefault(f,1) # ditch dupes
  176. ]
  177. def assert_relative(path):
  178. if not os.path.isabs(path):
  179. return path
  180. from distutils.errors import DistutilsSetupError
  181. msg = textwrap.dedent("""
  182. Error: setup script specifies an absolute path:
  183. %s
  184. setup() arguments must *always* be /-separated paths relative to the
  185. setup.py directory, *never* absolute paths.
  186. """).lstrip() % path
  187. raise DistutilsSetupError(msg)