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