##// END OF EJS Templates
Properly close file in setupbase
Matthias Bussonnier -
Show More
@@ -1,403 +1,404 b''
1 1 # encoding: utf-8
2 2 """
3 3 This module defines the things that are used in setup.py for building IPython
4 4
5 5 This includes:
6 6
7 7 * The basic arguments to setup
8 8 * Functions for finding things like packages, package data, etc.
9 9 * A function for checking dependencies.
10 10 """
11 11
12 12 # Copyright (c) IPython Development Team.
13 13 # Distributed under the terms of the Modified BSD License.
14 14
15 15
16 16 import re
17 17 import os
18 18 import sys
19 19
20 20 from distutils import log
21 21 from distutils.command.build_py import build_py
22 22 from distutils.command.build_scripts import build_scripts
23 23 from distutils.command.install import install
24 24 from distutils.command.install_scripts import install_scripts
25 25 from distutils.cmd import Command
26 26 from glob import glob
27 27
28 28 from setupext import install_data_ext
29 29
30 30 #-------------------------------------------------------------------------------
31 31 # Useful globals and utility functions
32 32 #-------------------------------------------------------------------------------
33 33
34 34 # A few handy globals
35 35 isfile = os.path.isfile
36 36 pjoin = os.path.join
37 37 repo_root = os.path.dirname(os.path.abspath(__file__))
38 38
39 39 def execfile(fname, globs, locs=None):
40 40 locs = locs or globs
41 exec(compile(open(fname).read(), fname, "exec"), globs, locs)
41 with open(fname) as f:
42 exec(compile(f.read(), fname, "exec"), globs, locs)
42 43
43 44 # A little utility we'll need below, since glob() does NOT allow you to do
44 45 # exclusion on multiple endings!
45 46 def file_doesnt_endwith(test,endings):
46 47 """Return true if test is a file and its name does NOT end with any
47 48 of the strings listed in endings."""
48 49 if not isfile(test):
49 50 return False
50 51 for e in endings:
51 52 if test.endswith(e):
52 53 return False
53 54 return True
54 55
55 56 #---------------------------------------------------------------------------
56 57 # Basic project information
57 58 #---------------------------------------------------------------------------
58 59
59 60 # release.py contains version, authors, license, url, keywords, etc.
60 61 execfile(pjoin(repo_root, 'IPython','core','release.py'), globals())
61 62
62 63 # Create a dict with the basic information
63 64 # This dict is eventually passed to setup after additional keys are added.
64 65 setup_args = dict(
65 66 name = name,
66 67 version = version,
67 68 description = description,
68 69 long_description = long_description,
69 70 author = author,
70 71 author_email = author_email,
71 72 url = url,
72 73 license = license,
73 74 platforms = platforms,
74 75 keywords = keywords,
75 76 classifiers = classifiers,
76 77 cmdclass = {'install_data': install_data_ext},
77 78 project_urls={
78 79 'Documentation': 'https://ipython.readthedocs.io/',
79 80 'Funding' : 'https://numfocus.org/',
80 81 'Source' : 'https://github.com/ipython/ipython',
81 82 'Tracker' : 'https://github.com/ipython/ipython/issues',
82 83 }
83 84 )
84 85
85 86
86 87 #---------------------------------------------------------------------------
87 88 # Find packages
88 89 #---------------------------------------------------------------------------
89 90
90 91 def find_packages():
91 92 """
92 93 Find all of IPython's packages.
93 94 """
94 95 excludes = ['deathrow', 'quarantine']
95 96 packages = []
96 97 for dir,subdirs,files in os.walk('IPython'):
97 98 package = dir.replace(os.path.sep, '.')
98 99 if any(package.startswith('IPython.'+exc) for exc in excludes):
99 100 # package is to be excluded (e.g. deathrow)
100 101 continue
101 102 if '__init__.py' not in files:
102 103 # not a package
103 104 continue
104 105 packages.append(package)
105 106 return packages
106 107
107 108 #---------------------------------------------------------------------------
108 109 # Find package data
109 110 #---------------------------------------------------------------------------
110 111
111 112 def find_package_data():
112 113 """
113 114 Find IPython's package_data.
114 115 """
115 116 # This is not enough for these things to appear in an sdist.
116 117 # We need to muck with the MANIFEST to get this to work
117 118
118 119 package_data = {
119 120 'IPython.core' : ['profile/README*'],
120 121 'IPython.core.tests' : ['*.png', '*.jpg', 'daft_extension/*.py'],
121 122 'IPython.lib.tests' : ['*.wav'],
122 123 'IPython.testing.plugin' : ['*.txt'],
123 124 }
124 125
125 126 return package_data
126 127
127 128
128 129 def check_package_data(package_data):
129 130 """verify that package_data globs make sense"""
130 131 print("checking package data")
131 132 for pkg, data in package_data.items():
132 133 pkg_root = pjoin(*pkg.split('.'))
133 134 for d in data:
134 135 path = pjoin(pkg_root, d)
135 136 if '*' in path:
136 137 assert len(glob(path)) > 0, "No files match pattern %s" % path
137 138 else:
138 139 assert os.path.exists(path), "Missing package data: %s" % path
139 140
140 141
141 142 def check_package_data_first(command):
142 143 """decorator for checking package_data before running a given command
143 144
144 145 Probably only needs to wrap build_py
145 146 """
146 147 class DecoratedCommand(command):
147 148 def run(self):
148 149 check_package_data(self.package_data)
149 150 command.run(self)
150 151 return DecoratedCommand
151 152
152 153
153 154 #---------------------------------------------------------------------------
154 155 # Find data files
155 156 #---------------------------------------------------------------------------
156 157
157 158 def find_data_files():
158 159 """
159 160 Find IPython's data_files.
160 161
161 162 Just man pages at this point.
162 163 """
163 164
164 165 manpagebase = pjoin('share', 'man', 'man1')
165 166
166 167 # Simple file lists can be made by hand
167 168 manpages = [f for f in glob(pjoin('docs','man','*.1.gz')) if isfile(f)]
168 169 if not manpages:
169 170 # When running from a source tree, the manpages aren't gzipped
170 171 manpages = [f for f in glob(pjoin('docs','man','*.1')) if isfile(f)]
171 172
172 173 # And assemble the entire output list
173 174 data_files = [ (manpagebase, manpages) ]
174 175
175 176 return data_files
176 177
177 178
178 179 # The two functions below are copied from IPython.utils.path, so we don't need
179 180 # to import IPython during setup, which fails on Python 3.
180 181
181 182 def target_outdated(target,deps):
182 183 """Determine whether a target is out of date.
183 184
184 185 target_outdated(target,deps) -> 1/0
185 186
186 187 deps: list of filenames which MUST exist.
187 188 target: single filename which may or may not exist.
188 189
189 190 If target doesn't exist or is older than any file listed in deps, return
190 191 true, otherwise return false.
191 192 """
192 193 try:
193 194 target_time = os.path.getmtime(target)
194 195 except os.error:
195 196 return 1
196 197 for dep in deps:
197 198 dep_time = os.path.getmtime(dep)
198 199 if dep_time > target_time:
199 200 #print "For target",target,"Dep failed:",dep # dbg
200 201 #print "times (dep,tar):",dep_time,target_time # dbg
201 202 return 1
202 203 return 0
203 204
204 205
205 206 def target_update(target,deps,cmd):
206 207 """Update a target with a given command given a list of dependencies.
207 208
208 209 target_update(target,deps,cmd) -> runs cmd if target is outdated.
209 210
210 211 This is just a wrapper around target_outdated() which calls the given
211 212 command if target is outdated."""
212 213
213 214 if target_outdated(target,deps):
214 215 os.system(cmd)
215 216
216 217 #---------------------------------------------------------------------------
217 218 # Find scripts
218 219 #---------------------------------------------------------------------------
219 220
220 221 def find_entry_points():
221 222 """Defines the command line entry points for IPython
222 223
223 224 This always uses setuptools-style entry points. When setuptools is not in
224 225 use, our own build_scripts_entrypt class below parses these and builds
225 226 command line scripts.
226 227
227 228 Each of our entry points gets both a plain name, e.g. ipython, and one
228 229 suffixed with the Python major version number, e.g. ipython3.
229 230 """
230 231 ep = [
231 232 'ipython%s = IPython:start_ipython',
232 233 'iptest%s = IPython.testing.iptestcontroller:main',
233 234 ]
234 235 suffix = str(sys.version_info[0])
235 236 return [e % '' for e in ep] + [e % suffix for e in ep]
236 237
237 238 script_src = """#!{executable}
238 239 # This script was automatically generated by setup.py
239 240 if __name__ == '__main__':
240 241 from {mod} import {func}
241 242 {func}()
242 243 """
243 244
244 245 class build_scripts_entrypt(build_scripts):
245 246 """Build the command line scripts
246 247
247 248 Parse setuptools style entry points and write simple scripts to run the
248 249 target functions.
249 250
250 251 On Windows, this also creates .cmd wrappers for the scripts so that you can
251 252 easily launch them from a command line.
252 253 """
253 254 def run(self):
254 255 self.mkpath(self.build_dir)
255 256 outfiles = []
256 257 for script in find_entry_points():
257 258 name, entrypt = script.split('=')
258 259 name = name.strip()
259 260 entrypt = entrypt.strip()
260 261 outfile = os.path.join(self.build_dir, name)
261 262 outfiles.append(outfile)
262 263 print('Writing script to', outfile)
263 264
264 265 mod, func = entrypt.split(':')
265 266 with open(outfile, 'w') as f:
266 267 f.write(script_src.format(executable=sys.executable,
267 268 mod=mod, func=func))
268 269
269 270 if sys.platform == 'win32':
270 271 # Write .cmd wrappers for Windows so 'ipython' etc. work at the
271 272 # command line
272 273 cmd_file = os.path.join(self.build_dir, name + '.cmd')
273 274 cmd = r'@"{python}" "%~dp0\{script}" %*\r\n'.format(
274 275 python=sys.executable, script=name)
275 276 log.info("Writing %s wrapper script" % cmd_file)
276 277 with open(cmd_file, 'w') as f:
277 278 f.write(cmd)
278 279
279 280 return outfiles, outfiles
280 281
281 282 class install_lib_symlink(Command):
282 283 user_options = [
283 284 ('install-dir=', 'd', "directory to install to"),
284 285 ]
285 286
286 287 def initialize_options(self):
287 288 self.install_dir = None
288 289
289 290 def finalize_options(self):
290 291 self.set_undefined_options('symlink',
291 292 ('install_lib', 'install_dir'),
292 293 )
293 294
294 295 def run(self):
295 296 if sys.platform == 'win32':
296 297 raise Exception("This doesn't work on Windows.")
297 298 pkg = os.path.join(os.getcwd(), 'IPython')
298 299 dest = os.path.join(self.install_dir, 'IPython')
299 300 if os.path.islink(dest):
300 301 print('removing existing symlink at %s' % dest)
301 302 os.unlink(dest)
302 303 print('symlinking %s -> %s' % (pkg, dest))
303 304 os.symlink(pkg, dest)
304 305
305 306 class unsymlink(install):
306 307 def run(self):
307 308 dest = os.path.join(self.install_lib, 'IPython')
308 309 if os.path.islink(dest):
309 310 print('removing symlink at %s' % dest)
310 311 os.unlink(dest)
311 312 else:
312 313 print('No symlink exists at %s' % dest)
313 314
314 315 class install_symlinked(install):
315 316 def run(self):
316 317 if sys.platform == 'win32':
317 318 raise Exception("This doesn't work on Windows.")
318 319
319 320 # Run all sub-commands (at least those that need to be run)
320 321 for cmd_name in self.get_sub_commands():
321 322 self.run_command(cmd_name)
322 323
323 324 # 'sub_commands': a list of commands this command might have to run to
324 325 # get its work done. See cmd.py for more info.
325 326 sub_commands = [('install_lib_symlink', lambda self:True),
326 327 ('install_scripts_sym', lambda self:True),
327 328 ]
328 329
329 330 class install_scripts_for_symlink(install_scripts):
330 331 """Redefined to get options from 'symlink' instead of 'install'.
331 332
332 333 I love distutils almost as much as I love setuptools.
333 334 """
334 335 def finalize_options(self):
335 336 self.set_undefined_options('build', ('build_scripts', 'build_dir'))
336 337 self.set_undefined_options('symlink',
337 338 ('install_scripts', 'install_dir'),
338 339 ('force', 'force'),
339 340 ('skip_build', 'skip_build'),
340 341 )
341 342
342 343
343 344 #---------------------------------------------------------------------------
344 345 # VCS related
345 346 #---------------------------------------------------------------------------
346 347
347 348
348 349 def git_prebuild(pkg_dir, build_cmd=build_py):
349 350 """Return extended build or sdist command class for recording commit
350 351
351 352 records git commit in IPython.utils._sysinfo.commit
352 353
353 354 for use in IPython.utils.sysinfo.sys_info() calls after installation.
354 355 """
355 356
356 357 class MyBuildPy(build_cmd):
357 358 ''' Subclass to write commit data into installation tree '''
358 359 def run(self):
359 360 # loose as `.dev` is suppose to be invalid
360 361 print("check version number")
361 362 loose_pep440re = re.compile(r'^(\d+)\.(\d+)\.(\d+((a|b|rc)\d+)?)(\.post\d+)?(\.dev\d*)?$')
362 363 if not loose_pep440re.match(version):
363 364 raise ValueError("Version number '%s' is not valid (should match [N!]N(.N)*[{a|b|rc}N][.postN][.devN])" % version)
364 365
365 366
366 367 build_cmd.run(self)
367 368 # this one will only fire for build commands
368 369 if hasattr(self, 'build_lib'):
369 370 self._record_commit(self.build_lib)
370 371
371 372 def make_release_tree(self, base_dir, files):
372 373 # this one will fire for sdist
373 374 build_cmd.make_release_tree(self, base_dir, files)
374 375 self._record_commit(base_dir)
375 376
376 377 def _record_commit(self, base_dir):
377 378 import subprocess
378 379 proc = subprocess.Popen('git rev-parse --short HEAD',
379 380 stdout=subprocess.PIPE,
380 381 stderr=subprocess.PIPE,
381 382 shell=True)
382 383 repo_commit, _ = proc.communicate()
383 384 repo_commit = repo_commit.strip().decode("ascii")
384 385
385 386 out_pth = pjoin(base_dir, pkg_dir, 'utils', '_sysinfo.py')
386 387 if os.path.isfile(out_pth) and not repo_commit:
387 388 # nothing to write, don't clobber
388 389 return
389 390
390 391 print("writing git commit '%s' to %s" % (repo_commit, out_pth))
391 392
392 393 # remove to avoid overwriting original via hard link
393 394 try:
394 395 os.remove(out_pth)
395 396 except (IOError, OSError):
396 397 pass
397 398 with open(out_pth, 'w') as out_file:
398 399 out_file.writelines([
399 400 '# GENERATED BY setup.py\n',
400 401 'commit = u"%s"\n' % repo_commit,
401 402 ])
402 403 return MyBuildPy
403 404
General Comments 0
You need to be logged in to leave comments. Login now