##// END OF EJS Templates
remove strict arg from css_js_prerelease
Min RK -
Show More
@@ -1,333 +1,332 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
2 # -*- coding: utf-8 -*-
3 """Setup script for IPython.
3 """Setup script for IPython.
4
4
5 Under Posix environments it works like a typical setup.py script.
5 Under Posix environments it works like a typical setup.py script.
6 Under Windows, the command sdist is not supported, since IPython
6 Under Windows, the command sdist is not supported, since IPython
7 requires utilities which are not available under Windows."""
7 requires utilities which are not available under Windows."""
8
8
9 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
10 # Copyright (c) 2008-2011, IPython Development Team.
10 # Copyright (c) 2008-2011, IPython Development Team.
11 # Copyright (c) 2001-2007, Fernando Perez <fernando.perez@colorado.edu>
11 # Copyright (c) 2001-2007, Fernando Perez <fernando.perez@colorado.edu>
12 # Copyright (c) 2001, Janko Hauser <jhauser@zscout.de>
12 # Copyright (c) 2001, Janko Hauser <jhauser@zscout.de>
13 # Copyright (c) 2001, Nathaniel Gray <n8gray@caltech.edu>
13 # Copyright (c) 2001, Nathaniel Gray <n8gray@caltech.edu>
14 #
14 #
15 # Distributed under the terms of the Modified BSD License.
15 # Distributed under the terms of the Modified BSD License.
16 #
16 #
17 # The full license is in the file COPYING.rst, distributed with this software.
17 # The full license is in the file COPYING.rst, distributed with this software.
18 #-----------------------------------------------------------------------------
18 #-----------------------------------------------------------------------------
19
19
20 #-----------------------------------------------------------------------------
20 #-----------------------------------------------------------------------------
21 # Minimal Python version sanity check
21 # Minimal Python version sanity check
22 #-----------------------------------------------------------------------------
22 #-----------------------------------------------------------------------------
23 from __future__ import print_function
23 from __future__ import print_function
24
24
25 import sys
25 import sys
26
26
27 # This check is also made in IPython/__init__, don't forget to update both when
27 # This check is also made in IPython/__init__, don't forget to update both when
28 # changing Python version requirements.
28 # changing Python version requirements.
29 v = sys.version_info
29 v = sys.version_info
30 if v[:2] < (2,7) or (v[0] >= 3 and v[:2] < (3,3)):
30 if v[:2] < (2,7) or (v[0] >= 3 and v[:2] < (3,3)):
31 error = "ERROR: IPython requires Python version 2.7 or 3.3 or above."
31 error = "ERROR: IPython requires Python version 2.7 or 3.3 or above."
32 print(error, file=sys.stderr)
32 print(error, file=sys.stderr)
33 sys.exit(1)
33 sys.exit(1)
34
34
35 PY3 = (sys.version_info[0] >= 3)
35 PY3 = (sys.version_info[0] >= 3)
36
36
37 # At least we're on the python version we need, move on.
37 # At least we're on the python version we need, move on.
38
38
39 #-------------------------------------------------------------------------------
39 #-------------------------------------------------------------------------------
40 # Imports
40 # Imports
41 #-------------------------------------------------------------------------------
41 #-------------------------------------------------------------------------------
42
42
43 # Stdlib imports
43 # Stdlib imports
44 import os
44 import os
45 import shutil
45 import shutil
46
46
47 from glob import glob
47 from glob import glob
48
48
49 # BEFORE importing distutils, remove MANIFEST. distutils doesn't properly
49 # BEFORE importing distutils, remove MANIFEST. distutils doesn't properly
50 # update it when the contents of directories change.
50 # update it when the contents of directories change.
51 if os.path.exists('MANIFEST'): os.remove('MANIFEST')
51 if os.path.exists('MANIFEST'): os.remove('MANIFEST')
52
52
53 from distutils.core import setup
53 from distutils.core import setup
54
54
55 # Our own imports
55 # Our own imports
56 from setupbase import target_update
56 from setupbase import target_update
57
57
58 from setupbase import (
58 from setupbase import (
59 setup_args,
59 setup_args,
60 find_packages,
60 find_packages,
61 find_package_data,
61 find_package_data,
62 check_package_data_first,
62 check_package_data_first,
63 find_entry_points,
63 find_entry_points,
64 build_scripts_entrypt,
64 build_scripts_entrypt,
65 find_data_files,
65 find_data_files,
66 check_for_dependencies,
66 check_for_dependencies,
67 git_prebuild,
67 git_prebuild,
68 check_submodule_status,
68 check_submodule_status,
69 update_submodules,
69 update_submodules,
70 require_submodules,
70 require_submodules,
71 UpdateSubmodules,
71 UpdateSubmodules,
72 get_bdist_wheel,
72 get_bdist_wheel,
73 CompileCSS,
73 CompileCSS,
74 JavascriptVersion,
74 JavascriptVersion,
75 css_js_prerelease,
75 css_js_prerelease,
76 install_symlinked,
76 install_symlinked,
77 install_lib_symlink,
77 install_lib_symlink,
78 install_scripts_for_symlink,
78 install_scripts_for_symlink,
79 unsymlink,
79 unsymlink,
80 )
80 )
81 from setupext import setupext
81 from setupext import setupext
82
82
83 isfile = os.path.isfile
83 isfile = os.path.isfile
84 pjoin = os.path.join
84 pjoin = os.path.join
85
85
86 #-------------------------------------------------------------------------------
86 #-------------------------------------------------------------------------------
87 # Handle OS specific things
87 # Handle OS specific things
88 #-------------------------------------------------------------------------------
88 #-------------------------------------------------------------------------------
89
89
90 if os.name in ('nt','dos'):
90 if os.name in ('nt','dos'):
91 os_name = 'windows'
91 os_name = 'windows'
92 else:
92 else:
93 os_name = os.name
93 os_name = os.name
94
94
95 # Under Windows, 'sdist' has not been supported. Now that the docs build with
95 # Under Windows, 'sdist' has not been supported. Now that the docs build with
96 # Sphinx it might work, but let's not turn it on until someone confirms that it
96 # Sphinx it might work, but let's not turn it on until someone confirms that it
97 # actually works.
97 # actually works.
98 if os_name == 'windows' and 'sdist' in sys.argv:
98 if os_name == 'windows' and 'sdist' in sys.argv:
99 print('The sdist command is not available under Windows. Exiting.')
99 print('The sdist command is not available under Windows. Exiting.')
100 sys.exit(1)
100 sys.exit(1)
101
101
102 #-------------------------------------------------------------------------------
102 #-------------------------------------------------------------------------------
103 # Make sure we aren't trying to run without submodules
103 # Make sure we aren't trying to run without submodules
104 #-------------------------------------------------------------------------------
104 #-------------------------------------------------------------------------------
105 here = os.path.abspath(os.path.dirname(__file__))
105 here = os.path.abspath(os.path.dirname(__file__))
106
106
107 def require_clean_submodules():
107 def require_clean_submodules():
108 """Check on git submodules before distutils can do anything
108 """Check on git submodules before distutils can do anything
109
109
110 Since distutils cannot be trusted to update the tree
110 Since distutils cannot be trusted to update the tree
111 after everything has been set in motion,
111 after everything has been set in motion,
112 this is not a distutils command.
112 this is not a distutils command.
113 """
113 """
114 # PACKAGERS: Add a return here to skip checks for git submodules
114 # PACKAGERS: Add a return here to skip checks for git submodules
115
115
116 # don't do anything if nothing is actually supposed to happen
116 # don't do anything if nothing is actually supposed to happen
117 for do_nothing in ('-h', '--help', '--help-commands', 'clean', 'submodule'):
117 for do_nothing in ('-h', '--help', '--help-commands', 'clean', 'submodule'):
118 if do_nothing in sys.argv:
118 if do_nothing in sys.argv:
119 return
119 return
120
120
121 status = check_submodule_status(here)
121 status = check_submodule_status(here)
122
122
123 if status == "missing":
123 if status == "missing":
124 print("checking out submodules for the first time")
124 print("checking out submodules for the first time")
125 update_submodules(here)
125 update_submodules(here)
126 elif status == "unclean":
126 elif status == "unclean":
127 print('\n'.join([
127 print('\n'.join([
128 "Cannot build / install IPython with unclean submodules",
128 "Cannot build / install IPython with unclean submodules",
129 "Please update submodules with",
129 "Please update submodules with",
130 " python setup.py submodule",
130 " python setup.py submodule",
131 "or",
131 "or",
132 " git submodule update",
132 " git submodule update",
133 "or commit any submodule changes you have made."
133 "or commit any submodule changes you have made."
134 ]))
134 ]))
135 sys.exit(1)
135 sys.exit(1)
136
136
137 require_clean_submodules()
137 require_clean_submodules()
138
138
139 #-------------------------------------------------------------------------------
139 #-------------------------------------------------------------------------------
140 # Things related to the IPython documentation
140 # Things related to the IPython documentation
141 #-------------------------------------------------------------------------------
141 #-------------------------------------------------------------------------------
142
142
143 # update the manuals when building a source dist
143 # update the manuals when building a source dist
144 if len(sys.argv) >= 2 and sys.argv[1] in ('sdist','bdist_rpm'):
144 if len(sys.argv) >= 2 and sys.argv[1] in ('sdist','bdist_rpm'):
145
145
146 # List of things to be updated. Each entry is a triplet of args for
146 # List of things to be updated. Each entry is a triplet of args for
147 # target_update()
147 # target_update()
148 to_update = [
148 to_update = [
149 # FIXME - Disabled for now: we need to redo an automatic way
149 # FIXME - Disabled for now: we need to redo an automatic way
150 # of generating the magic info inside the rst.
150 # of generating the magic info inside the rst.
151 #('docs/magic.tex',
151 #('docs/magic.tex',
152 #['IPython/Magic.py'],
152 #['IPython/Magic.py'],
153 #"cd doc && ./update_magic.sh" ),
153 #"cd doc && ./update_magic.sh" ),
154
154
155 ('docs/man/ipcluster.1.gz',
155 ('docs/man/ipcluster.1.gz',
156 ['docs/man/ipcluster.1'],
156 ['docs/man/ipcluster.1'],
157 'cd docs/man && gzip -9c ipcluster.1 > ipcluster.1.gz'),
157 'cd docs/man && gzip -9c ipcluster.1 > ipcluster.1.gz'),
158
158
159 ('docs/man/ipcontroller.1.gz',
159 ('docs/man/ipcontroller.1.gz',
160 ['docs/man/ipcontroller.1'],
160 ['docs/man/ipcontroller.1'],
161 'cd docs/man && gzip -9c ipcontroller.1 > ipcontroller.1.gz'),
161 'cd docs/man && gzip -9c ipcontroller.1 > ipcontroller.1.gz'),
162
162
163 ('docs/man/ipengine.1.gz',
163 ('docs/man/ipengine.1.gz',
164 ['docs/man/ipengine.1'],
164 ['docs/man/ipengine.1'],
165 'cd docs/man && gzip -9c ipengine.1 > ipengine.1.gz'),
165 'cd docs/man && gzip -9c ipengine.1 > ipengine.1.gz'),
166
166
167 ('docs/man/ipython.1.gz',
167 ('docs/man/ipython.1.gz',
168 ['docs/man/ipython.1'],
168 ['docs/man/ipython.1'],
169 'cd docs/man && gzip -9c ipython.1 > ipython.1.gz'),
169 'cd docs/man && gzip -9c ipython.1 > ipython.1.gz'),
170
170
171 ]
171 ]
172
172
173
173
174 [ target_update(*t) for t in to_update ]
174 [ target_update(*t) for t in to_update ]
175
175
176 #---------------------------------------------------------------------------
176 #---------------------------------------------------------------------------
177 # Find all the packages, package data, and data_files
177 # Find all the packages, package data, and data_files
178 #---------------------------------------------------------------------------
178 #---------------------------------------------------------------------------
179
179
180 packages = find_packages()
180 packages = find_packages()
181 package_data = find_package_data()
181 package_data = find_package_data()
182
182
183 data_files = find_data_files()
183 data_files = find_data_files()
184
184
185 setup_args['packages'] = packages
185 setup_args['packages'] = packages
186 setup_args['package_data'] = package_data
186 setup_args['package_data'] = package_data
187 setup_args['data_files'] = data_files
187 setup_args['data_files'] = data_files
188
188
189 #---------------------------------------------------------------------------
189 #---------------------------------------------------------------------------
190 # custom distutils commands
190 # custom distutils commands
191 #---------------------------------------------------------------------------
191 #---------------------------------------------------------------------------
192 # imports here, so they are after setuptools import if there was one
192 # imports here, so they are after setuptools import if there was one
193 from distutils.command.sdist import sdist
193 from distutils.command.sdist import sdist
194 from distutils.command.upload import upload
194 from distutils.command.upload import upload
195
195
196 class UploadWindowsInstallers(upload):
196 class UploadWindowsInstallers(upload):
197
197
198 description = "Upload Windows installers to PyPI (only used from tools/release_windows.py)"
198 description = "Upload Windows installers to PyPI (only used from tools/release_windows.py)"
199 user_options = upload.user_options + [
199 user_options = upload.user_options + [
200 ('files=', 'f', 'exe file (or glob) to upload')
200 ('files=', 'f', 'exe file (or glob) to upload')
201 ]
201 ]
202 def initialize_options(self):
202 def initialize_options(self):
203 upload.initialize_options(self)
203 upload.initialize_options(self)
204 meta = self.distribution.metadata
204 meta = self.distribution.metadata
205 base = '{name}-{version}'.format(
205 base = '{name}-{version}'.format(
206 name=meta.get_name(),
206 name=meta.get_name(),
207 version=meta.get_version()
207 version=meta.get_version()
208 )
208 )
209 self.files = os.path.join('dist', '%s.*.exe' % base)
209 self.files = os.path.join('dist', '%s.*.exe' % base)
210
210
211 def run(self):
211 def run(self):
212 for dist_file in glob(self.files):
212 for dist_file in glob(self.files):
213 self.upload_file('bdist_wininst', 'any', dist_file)
213 self.upload_file('bdist_wininst', 'any', dist_file)
214
214
215 setup_args['cmdclass'] = {
215 setup_args['cmdclass'] = {
216 'build_py': css_js_prerelease(
216 'build_py': css_js_prerelease(
217 check_package_data_first(git_prebuild('IPython')),
217 check_package_data_first(git_prebuild('IPython'))),
218 strict=False),
218 'sdist' : css_js_prerelease(git_prebuild('IPython', sdist)),
219 'sdist' : css_js_prerelease(git_prebuild('IPython', sdist), strict=False),
220 'upload_wininst' : UploadWindowsInstallers,
219 'upload_wininst' : UploadWindowsInstallers,
221 'submodule' : UpdateSubmodules,
220 'submodule' : UpdateSubmodules,
222 'css' : CompileCSS,
221 'css' : CompileCSS,
223 'symlink': install_symlinked,
222 'symlink': install_symlinked,
224 'install_lib_symlink': install_lib_symlink,
223 'install_lib_symlink': install_lib_symlink,
225 'install_scripts_sym': install_scripts_for_symlink,
224 'install_scripts_sym': install_scripts_for_symlink,
226 'unsymlink': unsymlink,
225 'unsymlink': unsymlink,
227 'jsversion' : JavascriptVersion,
226 'jsversion' : JavascriptVersion,
228 }
227 }
229
228
230 #---------------------------------------------------------------------------
229 #---------------------------------------------------------------------------
231 # Handle scripts, dependencies, and setuptools specific things
230 # Handle scripts, dependencies, and setuptools specific things
232 #---------------------------------------------------------------------------
231 #---------------------------------------------------------------------------
233
232
234 # For some commands, use setuptools. Note that we do NOT list install here!
233 # For some commands, use setuptools. Note that we do NOT list install here!
235 # If you want a setuptools-enhanced install, just run 'setupegg.py install'
234 # If you want a setuptools-enhanced install, just run 'setupegg.py install'
236 needs_setuptools = set(('develop', 'release', 'bdist_egg', 'bdist_rpm',
235 needs_setuptools = set(('develop', 'release', 'bdist_egg', 'bdist_rpm',
237 'bdist', 'bdist_dumb', 'bdist_wininst', 'bdist_wheel',
236 'bdist', 'bdist_dumb', 'bdist_wininst', 'bdist_wheel',
238 'egg_info', 'easy_install', 'upload', 'install_egg_info',
237 'egg_info', 'easy_install', 'upload', 'install_egg_info',
239 ))
238 ))
240
239
241 if len(needs_setuptools.intersection(sys.argv)) > 0:
240 if len(needs_setuptools.intersection(sys.argv)) > 0:
242 import setuptools
241 import setuptools
243
242
244 # This dict is used for passing extra arguments that are setuptools
243 # This dict is used for passing extra arguments that are setuptools
245 # specific to setup
244 # specific to setup
246 setuptools_extra_args = {}
245 setuptools_extra_args = {}
247
246
248 # setuptools requirements
247 # setuptools requirements
249
248
250 extras_require = dict(
249 extras_require = dict(
251 parallel = ['pyzmq>=2.1.11'],
250 parallel = ['pyzmq>=2.1.11'],
252 qtconsole = ['pyzmq>=2.1.11', 'pygments'],
251 qtconsole = ['pyzmq>=2.1.11', 'pygments'],
253 zmq = ['pyzmq>=2.1.11'],
252 zmq = ['pyzmq>=2.1.11'],
254 doc = ['Sphinx>=1.1', 'numpydoc'],
253 doc = ['Sphinx>=1.1', 'numpydoc'],
255 test = ['nose>=0.10.1', 'requests'],
254 test = ['nose>=0.10.1', 'requests'],
256 terminal = [],
255 terminal = [],
257 nbformat = ['jsonschema>=2.0'],
256 nbformat = ['jsonschema>=2.0'],
258 notebook = ['tornado>=4.0', 'pyzmq>=2.1.11', 'jinja2', 'pygments', 'mistune>=0.5'],
257 notebook = ['tornado>=4.0', 'pyzmq>=2.1.11', 'jinja2', 'pygments', 'mistune>=0.5'],
259 nbconvert = ['pygments', 'jinja2', 'mistune>=0.3.1']
258 nbconvert = ['pygments', 'jinja2', 'mistune>=0.3.1']
260 )
259 )
261
260
262 if sys.version_info < (3, 3):
261 if sys.version_info < (3, 3):
263 extras_require['test'].append('mock')
262 extras_require['test'].append('mock')
264
263
265 extras_require['notebook'].extend(extras_require['nbformat'])
264 extras_require['notebook'].extend(extras_require['nbformat'])
266 extras_require['nbconvert'].extend(extras_require['nbformat'])
265 extras_require['nbconvert'].extend(extras_require['nbformat'])
267
266
268 everything = set()
267 everything = set()
269 for deps in extras_require.values():
268 for deps in extras_require.values():
270 everything.update(deps)
269 everything.update(deps)
271 extras_require['all'] = everything
270 extras_require['all'] = everything
272
271
273 install_requires = []
272 install_requires = []
274
273
275 # add readline
274 # add readline
276 if sys.platform == 'darwin':
275 if sys.platform == 'darwin':
277 if 'bdist_wheel' in sys.argv[1:] or not setupext.check_for_readline():
276 if 'bdist_wheel' in sys.argv[1:] or not setupext.check_for_readline():
278 install_requires.append('gnureadline')
277 install_requires.append('gnureadline')
279 elif sys.platform.startswith('win'):
278 elif sys.platform.startswith('win'):
280 extras_require['terminal'].append('pyreadline>=2.0')
279 extras_require['terminal'].append('pyreadline>=2.0')
281
280
282
281
283 if 'setuptools' in sys.modules:
282 if 'setuptools' in sys.modules:
284 # setup.py develop should check for submodules
283 # setup.py develop should check for submodules
285 from setuptools.command.develop import develop
284 from setuptools.command.develop import develop
286 setup_args['cmdclass']['develop'] = require_submodules(develop)
285 setup_args['cmdclass']['develop'] = require_submodules(develop)
287 setup_args['cmdclass']['bdist_wheel'] = css_js_prerelease(get_bdist_wheel(), strict=False)
286 setup_args['cmdclass']['bdist_wheel'] = css_js_prerelease(get_bdist_wheel())
288
287
289 setuptools_extra_args['zip_safe'] = False
288 setuptools_extra_args['zip_safe'] = False
290 setuptools_extra_args['entry_points'] = {'console_scripts':find_entry_points()}
289 setuptools_extra_args['entry_points'] = {'console_scripts':find_entry_points()}
291 setup_args['extras_require'] = extras_require
290 setup_args['extras_require'] = extras_require
292 requires = setup_args['install_requires'] = install_requires
291 requires = setup_args['install_requires'] = install_requires
293
292
294 # Script to be run by the windows binary installer after the default setup
293 # Script to be run by the windows binary installer after the default setup
295 # routine, to add shortcuts and similar windows-only things. Windows
294 # routine, to add shortcuts and similar windows-only things. Windows
296 # post-install scripts MUST reside in the scripts/ dir, otherwise distutils
295 # post-install scripts MUST reside in the scripts/ dir, otherwise distutils
297 # doesn't find them.
296 # doesn't find them.
298 if 'bdist_wininst' in sys.argv:
297 if 'bdist_wininst' in sys.argv:
299 if len(sys.argv) > 2 and \
298 if len(sys.argv) > 2 and \
300 ('sdist' in sys.argv or 'bdist_rpm' in sys.argv):
299 ('sdist' in sys.argv or 'bdist_rpm' in sys.argv):
301 print("ERROR: bdist_wininst must be run alone. Exiting.", file=sys.stderr)
300 print("ERROR: bdist_wininst must be run alone. Exiting.", file=sys.stderr)
302 sys.exit(1)
301 sys.exit(1)
303 setup_args['data_files'].append(
302 setup_args['data_files'].append(
304 ['Scripts', ('scripts/ipython.ico', 'scripts/ipython_nb.ico')])
303 ['Scripts', ('scripts/ipython.ico', 'scripts/ipython_nb.ico')])
305 setup_args['scripts'] = [pjoin('scripts','ipython_win_post_install.py')]
304 setup_args['scripts'] = [pjoin('scripts','ipython_win_post_install.py')]
306 setup_args['options'] = {"bdist_wininst":
305 setup_args['options'] = {"bdist_wininst":
307 {"install_script":
306 {"install_script":
308 "ipython_win_post_install.py"}}
307 "ipython_win_post_install.py"}}
309
308
310 else:
309 else:
311 # If we are installing without setuptools, call this function which will
310 # If we are installing without setuptools, call this function which will
312 # check for dependencies an inform the user what is needed. This is
311 # check for dependencies an inform the user what is needed. This is
313 # just to make life easy for users.
312 # just to make life easy for users.
314 for install_cmd in ('install', 'symlink'):
313 for install_cmd in ('install', 'symlink'):
315 if install_cmd in sys.argv:
314 if install_cmd in sys.argv:
316 check_for_dependencies()
315 check_for_dependencies()
317 break
316 break
318 # scripts has to be a non-empty list, or install_scripts isn't called
317 # scripts has to be a non-empty list, or install_scripts isn't called
319 setup_args['scripts'] = [e.split('=')[0].strip() for e in find_entry_points()]
318 setup_args['scripts'] = [e.split('=')[0].strip() for e in find_entry_points()]
320
319
321 setup_args['cmdclass']['build_scripts'] = build_scripts_entrypt
320 setup_args['cmdclass']['build_scripts'] = build_scripts_entrypt
322
321
323 #---------------------------------------------------------------------------
322 #---------------------------------------------------------------------------
324 # Do the actual setup now
323 # Do the actual setup now
325 #---------------------------------------------------------------------------
324 #---------------------------------------------------------------------------
326
325
327 setup_args.update(setuptools_extra_args)
326 setup_args.update(setuptools_extra_args)
328
327
329 def main():
328 def main():
330 setup(**setup_args)
329 setup(**setup_args)
331
330
332 if __name__ == '__main__':
331 if __name__ == '__main__':
333 main()
332 main()
@@ -1,765 +1,762 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 """
2 """
3 This module defines the things that are used in setup.py for building IPython
3 This module defines the things that are used in setup.py for building IPython
4
4
5 This includes:
5 This includes:
6
6
7 * The basic arguments to setup
7 * The basic arguments to setup
8 * Functions for finding things like packages, package data, etc.
8 * Functions for finding things like packages, package data, etc.
9 * A function for checking dependencies.
9 * A function for checking dependencies.
10 """
10 """
11
11
12 # Copyright (c) IPython Development Team.
12 # Copyright (c) IPython Development Team.
13 # Distributed under the terms of the Modified BSD License.
13 # Distributed under the terms of the Modified BSD License.
14
14
15 from __future__ import print_function
15 from __future__ import print_function
16
16
17 import errno
17 import errno
18 import os
18 import os
19 import sys
19 import sys
20
20
21 from distutils import log
21 from distutils import log
22 from distutils.command.build_py import build_py
22 from distutils.command.build_py import build_py
23 from distutils.command.build_scripts import build_scripts
23 from distutils.command.build_scripts import build_scripts
24 from distutils.command.install import install
24 from distutils.command.install import install
25 from distutils.command.install_scripts import install_scripts
25 from distutils.command.install_scripts import install_scripts
26 from distutils.cmd import Command
26 from distutils.cmd import Command
27 from distutils.errors import DistutilsExecError
27 from distutils.errors import DistutilsExecError
28 from fnmatch import fnmatch
28 from fnmatch import fnmatch
29 from glob import glob
29 from glob import glob
30 from subprocess import Popen, PIPE
30 from subprocess import Popen, PIPE
31
31
32 from setupext import install_data_ext
32 from setupext import install_data_ext
33
33
34 #-------------------------------------------------------------------------------
34 #-------------------------------------------------------------------------------
35 # Useful globals and utility functions
35 # Useful globals and utility functions
36 #-------------------------------------------------------------------------------
36 #-------------------------------------------------------------------------------
37
37
38 # A few handy globals
38 # A few handy globals
39 isfile = os.path.isfile
39 isfile = os.path.isfile
40 pjoin = os.path.join
40 pjoin = os.path.join
41 repo_root = os.path.dirname(os.path.abspath(__file__))
41 repo_root = os.path.dirname(os.path.abspath(__file__))
42
42
43 def oscmd(s):
43 def oscmd(s):
44 print(">", s)
44 print(">", s)
45 os.system(s)
45 os.system(s)
46
46
47 # Py3 compatibility hacks, without assuming IPython itself is installed with
47 # Py3 compatibility hacks, without assuming IPython itself is installed with
48 # the full py3compat machinery.
48 # the full py3compat machinery.
49
49
50 try:
50 try:
51 execfile
51 execfile
52 except NameError:
52 except NameError:
53 def execfile(fname, globs, locs=None):
53 def execfile(fname, globs, locs=None):
54 locs = locs or globs
54 locs = locs or globs
55 exec(compile(open(fname).read(), fname, "exec"), globs, locs)
55 exec(compile(open(fname).read(), fname, "exec"), globs, locs)
56
56
57 # A little utility we'll need below, since glob() does NOT allow you to do
57 # A little utility we'll need below, since glob() does NOT allow you to do
58 # exclusion on multiple endings!
58 # exclusion on multiple endings!
59 def file_doesnt_endwith(test,endings):
59 def file_doesnt_endwith(test,endings):
60 """Return true if test is a file and its name does NOT end with any
60 """Return true if test is a file and its name does NOT end with any
61 of the strings listed in endings."""
61 of the strings listed in endings."""
62 if not isfile(test):
62 if not isfile(test):
63 return False
63 return False
64 for e in endings:
64 for e in endings:
65 if test.endswith(e):
65 if test.endswith(e):
66 return False
66 return False
67 return True
67 return True
68
68
69 #---------------------------------------------------------------------------
69 #---------------------------------------------------------------------------
70 # Basic project information
70 # Basic project information
71 #---------------------------------------------------------------------------
71 #---------------------------------------------------------------------------
72
72
73 # release.py contains version, authors, license, url, keywords, etc.
73 # release.py contains version, authors, license, url, keywords, etc.
74 execfile(pjoin(repo_root, 'IPython','core','release.py'), globals())
74 execfile(pjoin(repo_root, 'IPython','core','release.py'), globals())
75
75
76 # Create a dict with the basic information
76 # Create a dict with the basic information
77 # This dict is eventually passed to setup after additional keys are added.
77 # This dict is eventually passed to setup after additional keys are added.
78 setup_args = dict(
78 setup_args = dict(
79 name = name,
79 name = name,
80 version = version,
80 version = version,
81 description = description,
81 description = description,
82 long_description = long_description,
82 long_description = long_description,
83 author = author,
83 author = author,
84 author_email = author_email,
84 author_email = author_email,
85 url = url,
85 url = url,
86 download_url = download_url,
86 download_url = download_url,
87 license = license,
87 license = license,
88 platforms = platforms,
88 platforms = platforms,
89 keywords = keywords,
89 keywords = keywords,
90 classifiers = classifiers,
90 classifiers = classifiers,
91 cmdclass = {'install_data': install_data_ext},
91 cmdclass = {'install_data': install_data_ext},
92 )
92 )
93
93
94
94
95 #---------------------------------------------------------------------------
95 #---------------------------------------------------------------------------
96 # Find packages
96 # Find packages
97 #---------------------------------------------------------------------------
97 #---------------------------------------------------------------------------
98
98
99 def find_packages():
99 def find_packages():
100 """
100 """
101 Find all of IPython's packages.
101 Find all of IPython's packages.
102 """
102 """
103 excludes = ['deathrow', 'quarantine']
103 excludes = ['deathrow', 'quarantine']
104 packages = []
104 packages = []
105 for dir,subdirs,files in os.walk('IPython'):
105 for dir,subdirs,files in os.walk('IPython'):
106 package = dir.replace(os.path.sep, '.')
106 package = dir.replace(os.path.sep, '.')
107 if any(package.startswith('IPython.'+exc) for exc in excludes):
107 if any(package.startswith('IPython.'+exc) for exc in excludes):
108 # package is to be excluded (e.g. deathrow)
108 # package is to be excluded (e.g. deathrow)
109 continue
109 continue
110 if '__init__.py' not in files:
110 if '__init__.py' not in files:
111 # not a package
111 # not a package
112 continue
112 continue
113 packages.append(package)
113 packages.append(package)
114 return packages
114 return packages
115
115
116 #---------------------------------------------------------------------------
116 #---------------------------------------------------------------------------
117 # Find package data
117 # Find package data
118 #---------------------------------------------------------------------------
118 #---------------------------------------------------------------------------
119
119
120 def find_package_data():
120 def find_package_data():
121 """
121 """
122 Find IPython's package_data.
122 Find IPython's package_data.
123 """
123 """
124 # This is not enough for these things to appear in an sdist.
124 # This is not enough for these things to appear in an sdist.
125 # We need to muck with the MANIFEST to get this to work
125 # We need to muck with the MANIFEST to get this to work
126
126
127 # exclude components and less from the walk;
127 # exclude components and less from the walk;
128 # we will build the components separately
128 # we will build the components separately
129 excludes = [
129 excludes = [
130 pjoin('static', 'components'),
130 pjoin('static', 'components'),
131 pjoin('static', '*', 'less'),
131 pjoin('static', '*', 'less'),
132 ]
132 ]
133
133
134 # walk notebook resources:
134 # walk notebook resources:
135 cwd = os.getcwd()
135 cwd = os.getcwd()
136 os.chdir(os.path.join('IPython', 'html'))
136 os.chdir(os.path.join('IPython', 'html'))
137 static_data = []
137 static_data = []
138 for parent, dirs, files in os.walk('static'):
138 for parent, dirs, files in os.walk('static'):
139 if any(fnmatch(parent, pat) for pat in excludes):
139 if any(fnmatch(parent, pat) for pat in excludes):
140 # prevent descending into subdirs
140 # prevent descending into subdirs
141 dirs[:] = []
141 dirs[:] = []
142 continue
142 continue
143 for f in files:
143 for f in files:
144 static_data.append(pjoin(parent, f))
144 static_data.append(pjoin(parent, f))
145
145
146 components = pjoin("static", "components")
146 components = pjoin("static", "components")
147 # select the components we actually need to install
147 # select the components we actually need to install
148 # (there are lots of resources we bundle for sdist-reasons that we don't actually use)
148 # (there are lots of resources we bundle for sdist-reasons that we don't actually use)
149 static_data.extend([
149 static_data.extend([
150 pjoin(components, "backbone", "backbone-min.js"),
150 pjoin(components, "backbone", "backbone-min.js"),
151 pjoin(components, "bootstrap", "js", "bootstrap.min.js"),
151 pjoin(components, "bootstrap", "js", "bootstrap.min.js"),
152 pjoin(components, "bootstrap-tour", "build", "css", "bootstrap-tour.min.css"),
152 pjoin(components, "bootstrap-tour", "build", "css", "bootstrap-tour.min.css"),
153 pjoin(components, "bootstrap-tour", "build", "js", "bootstrap-tour.min.js"),
153 pjoin(components, "bootstrap-tour", "build", "js", "bootstrap-tour.min.js"),
154 pjoin(components, "es6-promise", "*.js"),
154 pjoin(components, "es6-promise", "*.js"),
155 pjoin(components, "font-awesome", "fonts", "*.*"),
155 pjoin(components, "font-awesome", "fonts", "*.*"),
156 pjoin(components, "google-caja", "html-css-sanitizer-minified.js"),
156 pjoin(components, "google-caja", "html-css-sanitizer-minified.js"),
157 pjoin(components, "jquery", "jquery.min.js"),
157 pjoin(components, "jquery", "jquery.min.js"),
158 pjoin(components, "jquery-ui", "ui", "minified", "jquery-ui.min.js"),
158 pjoin(components, "jquery-ui", "ui", "minified", "jquery-ui.min.js"),
159 pjoin(components, "jquery-ui", "themes", "smoothness", "jquery-ui.min.css"),
159 pjoin(components, "jquery-ui", "themes", "smoothness", "jquery-ui.min.css"),
160 pjoin(components, "jquery-ui", "themes", "smoothness", "images", "*"),
160 pjoin(components, "jquery-ui", "themes", "smoothness", "images", "*"),
161 pjoin(components, "marked", "lib", "marked.js"),
161 pjoin(components, "marked", "lib", "marked.js"),
162 pjoin(components, "requirejs", "require.js"),
162 pjoin(components, "requirejs", "require.js"),
163 pjoin(components, "underscore", "underscore-min.js"),
163 pjoin(components, "underscore", "underscore-min.js"),
164 pjoin(components, "moment", "moment.js"),
164 pjoin(components, "moment", "moment.js"),
165 pjoin(components, "moment", "min", "moment.min.js"),
165 pjoin(components, "moment", "min", "moment.min.js"),
166 pjoin(components, "term.js", "src", "term.js"),
166 pjoin(components, "term.js", "src", "term.js"),
167 pjoin(components, "text-encoding", "lib", "encoding.js"),
167 pjoin(components, "text-encoding", "lib", "encoding.js"),
168 ])
168 ])
169
169
170 # Ship all of Codemirror's CSS and JS
170 # Ship all of Codemirror's CSS and JS
171 for parent, dirs, files in os.walk(pjoin(components, 'codemirror')):
171 for parent, dirs, files in os.walk(pjoin(components, 'codemirror')):
172 for f in files:
172 for f in files:
173 if f.endswith(('.js', '.css')):
173 if f.endswith(('.js', '.css')):
174 static_data.append(pjoin(parent, f))
174 static_data.append(pjoin(parent, f))
175
175
176 os.chdir(os.path.join('tests',))
176 os.chdir(os.path.join('tests',))
177 js_tests = glob('*.js') + glob('*/*.js')
177 js_tests = glob('*.js') + glob('*/*.js')
178
178
179 os.chdir(os.path.join(cwd, 'IPython', 'nbconvert'))
179 os.chdir(os.path.join(cwd, 'IPython', 'nbconvert'))
180 nbconvert_templates = [os.path.join(dirpath, '*.*')
180 nbconvert_templates = [os.path.join(dirpath, '*.*')
181 for dirpath, _, _ in os.walk('templates')]
181 for dirpath, _, _ in os.walk('templates')]
182
182
183 os.chdir(cwd)
183 os.chdir(cwd)
184
184
185 package_data = {
185 package_data = {
186 'IPython.config.profile' : ['README*', '*/*.py'],
186 'IPython.config.profile' : ['README*', '*/*.py'],
187 'IPython.core.tests' : ['*.png', '*.jpg'],
187 'IPython.core.tests' : ['*.png', '*.jpg'],
188 'IPython.lib.tests' : ['*.wav'],
188 'IPython.lib.tests' : ['*.wav'],
189 'IPython.testing.plugin' : ['*.txt'],
189 'IPython.testing.plugin' : ['*.txt'],
190 'IPython.html' : ['templates/*'] + static_data,
190 'IPython.html' : ['templates/*'] + static_data,
191 'IPython.html.tests' : js_tests,
191 'IPython.html.tests' : js_tests,
192 'IPython.qt.console' : ['resources/icon/*.svg'],
192 'IPython.qt.console' : ['resources/icon/*.svg'],
193 'IPython.nbconvert' : nbconvert_templates +
193 'IPython.nbconvert' : nbconvert_templates +
194 [
194 [
195 'tests/files/*.*',
195 'tests/files/*.*',
196 'exporters/tests/files/*.*',
196 'exporters/tests/files/*.*',
197 'preprocessors/tests/files/*.*',
197 'preprocessors/tests/files/*.*',
198 ],
198 ],
199 'IPython.nbconvert.filters' : ['marked.js'],
199 'IPython.nbconvert.filters' : ['marked.js'],
200 'IPython.nbformat' : [
200 'IPython.nbformat' : [
201 'tests/*.ipynb',
201 'tests/*.ipynb',
202 'v3/nbformat.v3.schema.json',
202 'v3/nbformat.v3.schema.json',
203 'v4/nbformat.v4.schema.json',
203 'v4/nbformat.v4.schema.json',
204 ],
204 ],
205 'IPython.kernel': ['resources/*.*'],
205 'IPython.kernel': ['resources/*.*'],
206 }
206 }
207
207
208 return package_data
208 return package_data
209
209
210
210
211 def check_package_data(package_data):
211 def check_package_data(package_data):
212 """verify that package_data globs make sense"""
212 """verify that package_data globs make sense"""
213 print("checking package data")
213 print("checking package data")
214 for pkg, data in package_data.items():
214 for pkg, data in package_data.items():
215 pkg_root = pjoin(*pkg.split('.'))
215 pkg_root = pjoin(*pkg.split('.'))
216 for d in data:
216 for d in data:
217 path = pjoin(pkg_root, d)
217 path = pjoin(pkg_root, d)
218 if '*' in path:
218 if '*' in path:
219 assert len(glob(path)) > 0, "No files match pattern %s" % path
219 assert len(glob(path)) > 0, "No files match pattern %s" % path
220 else:
220 else:
221 assert os.path.exists(path), "Missing package data: %s" % path
221 assert os.path.exists(path), "Missing package data: %s" % path
222
222
223
223
224 def check_package_data_first(command):
224 def check_package_data_first(command):
225 """decorator for checking package_data before running a given command
225 """decorator for checking package_data before running a given command
226
226
227 Probably only needs to wrap build_py
227 Probably only needs to wrap build_py
228 """
228 """
229 class DecoratedCommand(command):
229 class DecoratedCommand(command):
230 def run(self):
230 def run(self):
231 check_package_data(self.package_data)
231 check_package_data(self.package_data)
232 command.run(self)
232 command.run(self)
233 return DecoratedCommand
233 return DecoratedCommand
234
234
235
235
236 #---------------------------------------------------------------------------
236 #---------------------------------------------------------------------------
237 # Find data files
237 # Find data files
238 #---------------------------------------------------------------------------
238 #---------------------------------------------------------------------------
239
239
240 def make_dir_struct(tag,base,out_base):
240 def make_dir_struct(tag,base,out_base):
241 """Make the directory structure of all files below a starting dir.
241 """Make the directory structure of all files below a starting dir.
242
242
243 This is just a convenience routine to help build a nested directory
243 This is just a convenience routine to help build a nested directory
244 hierarchy because distutils is too stupid to do this by itself.
244 hierarchy because distutils is too stupid to do this by itself.
245
245
246 XXX - this needs a proper docstring!
246 XXX - this needs a proper docstring!
247 """
247 """
248
248
249 # we'll use these a lot below
249 # we'll use these a lot below
250 lbase = len(base)
250 lbase = len(base)
251 pathsep = os.path.sep
251 pathsep = os.path.sep
252 lpathsep = len(pathsep)
252 lpathsep = len(pathsep)
253
253
254 out = []
254 out = []
255 for (dirpath,dirnames,filenames) in os.walk(base):
255 for (dirpath,dirnames,filenames) in os.walk(base):
256 # we need to strip out the dirpath from the base to map it to the
256 # we need to strip out the dirpath from the base to map it to the
257 # output (installation) path. This requires possibly stripping the
257 # output (installation) path. This requires possibly stripping the
258 # path separator, because otherwise pjoin will not work correctly
258 # path separator, because otherwise pjoin will not work correctly
259 # (pjoin('foo/','/bar') returns '/bar').
259 # (pjoin('foo/','/bar') returns '/bar').
260
260
261 dp_eff = dirpath[lbase:]
261 dp_eff = dirpath[lbase:]
262 if dp_eff.startswith(pathsep):
262 if dp_eff.startswith(pathsep):
263 dp_eff = dp_eff[lpathsep:]
263 dp_eff = dp_eff[lpathsep:]
264 # The output path must be anchored at the out_base marker
264 # The output path must be anchored at the out_base marker
265 out_path = pjoin(out_base,dp_eff)
265 out_path = pjoin(out_base,dp_eff)
266 # Now we can generate the final filenames. Since os.walk only produces
266 # Now we can generate the final filenames. Since os.walk only produces
267 # filenames, we must join back with the dirpath to get full valid file
267 # filenames, we must join back with the dirpath to get full valid file
268 # paths:
268 # paths:
269 pfiles = [pjoin(dirpath,f) for f in filenames]
269 pfiles = [pjoin(dirpath,f) for f in filenames]
270 # Finally, generate the entry we need, which is a pari of (output
270 # Finally, generate the entry we need, which is a pari of (output
271 # path, files) for use as a data_files parameter in install_data.
271 # path, files) for use as a data_files parameter in install_data.
272 out.append((out_path, pfiles))
272 out.append((out_path, pfiles))
273
273
274 return out
274 return out
275
275
276
276
277 def find_data_files():
277 def find_data_files():
278 """
278 """
279 Find IPython's data_files.
279 Find IPython's data_files.
280
280
281 Just man pages at this point.
281 Just man pages at this point.
282 """
282 """
283
283
284 manpagebase = pjoin('share', 'man', 'man1')
284 manpagebase = pjoin('share', 'man', 'man1')
285
285
286 # Simple file lists can be made by hand
286 # Simple file lists can be made by hand
287 manpages = [f for f in glob(pjoin('docs','man','*.1.gz')) if isfile(f)]
287 manpages = [f for f in glob(pjoin('docs','man','*.1.gz')) if isfile(f)]
288 if not manpages:
288 if not manpages:
289 # When running from a source tree, the manpages aren't gzipped
289 # When running from a source tree, the manpages aren't gzipped
290 manpages = [f for f in glob(pjoin('docs','man','*.1')) if isfile(f)]
290 manpages = [f for f in glob(pjoin('docs','man','*.1')) if isfile(f)]
291
291
292 # And assemble the entire output list
292 # And assemble the entire output list
293 data_files = [ (manpagebase, manpages) ]
293 data_files = [ (manpagebase, manpages) ]
294
294
295 return data_files
295 return data_files
296
296
297
297
298 def make_man_update_target(manpage):
298 def make_man_update_target(manpage):
299 """Return a target_update-compliant tuple for the given manpage.
299 """Return a target_update-compliant tuple for the given manpage.
300
300
301 Parameters
301 Parameters
302 ----------
302 ----------
303 manpage : string
303 manpage : string
304 Name of the manpage, must include the section number (trailing number).
304 Name of the manpage, must include the section number (trailing number).
305
305
306 Example
306 Example
307 -------
307 -------
308
308
309 >>> make_man_update_target('ipython.1') #doctest: +NORMALIZE_WHITESPACE
309 >>> make_man_update_target('ipython.1') #doctest: +NORMALIZE_WHITESPACE
310 ('docs/man/ipython.1.gz',
310 ('docs/man/ipython.1.gz',
311 ['docs/man/ipython.1'],
311 ['docs/man/ipython.1'],
312 'cd docs/man && gzip -9c ipython.1 > ipython.1.gz')
312 'cd docs/man && gzip -9c ipython.1 > ipython.1.gz')
313 """
313 """
314 man_dir = pjoin('docs', 'man')
314 man_dir = pjoin('docs', 'man')
315 manpage_gz = manpage + '.gz'
315 manpage_gz = manpage + '.gz'
316 manpath = pjoin(man_dir, manpage)
316 manpath = pjoin(man_dir, manpage)
317 manpath_gz = pjoin(man_dir, manpage_gz)
317 manpath_gz = pjoin(man_dir, manpage_gz)
318 gz_cmd = ( "cd %(man_dir)s && gzip -9c %(manpage)s > %(manpage_gz)s" %
318 gz_cmd = ( "cd %(man_dir)s && gzip -9c %(manpage)s > %(manpage_gz)s" %
319 locals() )
319 locals() )
320 return (manpath_gz, [manpath], gz_cmd)
320 return (manpath_gz, [manpath], gz_cmd)
321
321
322 # The two functions below are copied from IPython.utils.path, so we don't need
322 # The two functions below are copied from IPython.utils.path, so we don't need
323 # to import IPython during setup, which fails on Python 3.
323 # to import IPython during setup, which fails on Python 3.
324
324
325 def target_outdated(target,deps):
325 def target_outdated(target,deps):
326 """Determine whether a target is out of date.
326 """Determine whether a target is out of date.
327
327
328 target_outdated(target,deps) -> 1/0
328 target_outdated(target,deps) -> 1/0
329
329
330 deps: list of filenames which MUST exist.
330 deps: list of filenames which MUST exist.
331 target: single filename which may or may not exist.
331 target: single filename which may or may not exist.
332
332
333 If target doesn't exist or is older than any file listed in deps, return
333 If target doesn't exist or is older than any file listed in deps, return
334 true, otherwise return false.
334 true, otherwise return false.
335 """
335 """
336 try:
336 try:
337 target_time = os.path.getmtime(target)
337 target_time = os.path.getmtime(target)
338 except os.error:
338 except os.error:
339 return 1
339 return 1
340 for dep in deps:
340 for dep in deps:
341 dep_time = os.path.getmtime(dep)
341 dep_time = os.path.getmtime(dep)
342 if dep_time > target_time:
342 if dep_time > target_time:
343 #print "For target",target,"Dep failed:",dep # dbg
343 #print "For target",target,"Dep failed:",dep # dbg
344 #print "times (dep,tar):",dep_time,target_time # dbg
344 #print "times (dep,tar):",dep_time,target_time # dbg
345 return 1
345 return 1
346 return 0
346 return 0
347
347
348
348
349 def target_update(target,deps,cmd):
349 def target_update(target,deps,cmd):
350 """Update a target with a given command given a list of dependencies.
350 """Update a target with a given command given a list of dependencies.
351
351
352 target_update(target,deps,cmd) -> runs cmd if target is outdated.
352 target_update(target,deps,cmd) -> runs cmd if target is outdated.
353
353
354 This is just a wrapper around target_outdated() which calls the given
354 This is just a wrapper around target_outdated() which calls the given
355 command if target is outdated."""
355 command if target is outdated."""
356
356
357 if target_outdated(target,deps):
357 if target_outdated(target,deps):
358 os.system(cmd)
358 os.system(cmd)
359
359
360 #---------------------------------------------------------------------------
360 #---------------------------------------------------------------------------
361 # Find scripts
361 # Find scripts
362 #---------------------------------------------------------------------------
362 #---------------------------------------------------------------------------
363
363
364 def find_entry_points():
364 def find_entry_points():
365 """Defines the command line entry points for IPython
365 """Defines the command line entry points for IPython
366
366
367 This always uses setuptools-style entry points. When setuptools is not in
367 This always uses setuptools-style entry points. When setuptools is not in
368 use, our own build_scripts_entrypt class below parses these and builds
368 use, our own build_scripts_entrypt class below parses these and builds
369 command line scripts.
369 command line scripts.
370
370
371 Each of our entry points gets both a plain name, e.g. ipython, and one
371 Each of our entry points gets both a plain name, e.g. ipython, and one
372 suffixed with the Python major version number, e.g. ipython3.
372 suffixed with the Python major version number, e.g. ipython3.
373 """
373 """
374 ep = [
374 ep = [
375 'ipython%s = IPython:start_ipython',
375 'ipython%s = IPython:start_ipython',
376 'ipcontroller%s = IPython.parallel.apps.ipcontrollerapp:launch_new_instance',
376 'ipcontroller%s = IPython.parallel.apps.ipcontrollerapp:launch_new_instance',
377 'ipengine%s = IPython.parallel.apps.ipengineapp:launch_new_instance',
377 'ipengine%s = IPython.parallel.apps.ipengineapp:launch_new_instance',
378 'ipcluster%s = IPython.parallel.apps.ipclusterapp:launch_new_instance',
378 'ipcluster%s = IPython.parallel.apps.ipclusterapp:launch_new_instance',
379 'iptest%s = IPython.testing.iptestcontroller:main',
379 'iptest%s = IPython.testing.iptestcontroller:main',
380 ]
380 ]
381 suffix = str(sys.version_info[0])
381 suffix = str(sys.version_info[0])
382 return [e % '' for e in ep] + [e % suffix for e in ep]
382 return [e % '' for e in ep] + [e % suffix for e in ep]
383
383
384 script_src = """#!{executable}
384 script_src = """#!{executable}
385 # This script was automatically generated by setup.py
385 # This script was automatically generated by setup.py
386 if __name__ == '__main__':
386 if __name__ == '__main__':
387 from {mod} import {func}
387 from {mod} import {func}
388 {func}()
388 {func}()
389 """
389 """
390
390
391 class build_scripts_entrypt(build_scripts):
391 class build_scripts_entrypt(build_scripts):
392 """Build the command line scripts
392 """Build the command line scripts
393
393
394 Parse setuptools style entry points and write simple scripts to run the
394 Parse setuptools style entry points and write simple scripts to run the
395 target functions.
395 target functions.
396
396
397 On Windows, this also creates .cmd wrappers for the scripts so that you can
397 On Windows, this also creates .cmd wrappers for the scripts so that you can
398 easily launch them from a command line.
398 easily launch them from a command line.
399 """
399 """
400 def run(self):
400 def run(self):
401 self.mkpath(self.build_dir)
401 self.mkpath(self.build_dir)
402 outfiles = []
402 outfiles = []
403 for script in find_entry_points():
403 for script in find_entry_points():
404 name, entrypt = script.split('=')
404 name, entrypt = script.split('=')
405 name = name.strip()
405 name = name.strip()
406 entrypt = entrypt.strip()
406 entrypt = entrypt.strip()
407 outfile = os.path.join(self.build_dir, name)
407 outfile = os.path.join(self.build_dir, name)
408 outfiles.append(outfile)
408 outfiles.append(outfile)
409 print('Writing script to', outfile)
409 print('Writing script to', outfile)
410
410
411 mod, func = entrypt.split(':')
411 mod, func = entrypt.split(':')
412 with open(outfile, 'w') as f:
412 with open(outfile, 'w') as f:
413 f.write(script_src.format(executable=sys.executable,
413 f.write(script_src.format(executable=sys.executable,
414 mod=mod, func=func))
414 mod=mod, func=func))
415
415
416 if sys.platform == 'win32':
416 if sys.platform == 'win32':
417 # Write .cmd wrappers for Windows so 'ipython' etc. work at the
417 # Write .cmd wrappers for Windows so 'ipython' etc. work at the
418 # command line
418 # command line
419 cmd_file = os.path.join(self.build_dir, name + '.cmd')
419 cmd_file = os.path.join(self.build_dir, name + '.cmd')
420 cmd = '@"{python}" "%~dp0\{script}" %*\r\n'.format(
420 cmd = '@"{python}" "%~dp0\{script}" %*\r\n'.format(
421 python=sys.executable, script=name)
421 python=sys.executable, script=name)
422 log.info("Writing %s wrapper script" % cmd_file)
422 log.info("Writing %s wrapper script" % cmd_file)
423 with open(cmd_file, 'w') as f:
423 with open(cmd_file, 'w') as f:
424 f.write(cmd)
424 f.write(cmd)
425
425
426 return outfiles, outfiles
426 return outfiles, outfiles
427
427
428 class install_lib_symlink(Command):
428 class install_lib_symlink(Command):
429 user_options = [
429 user_options = [
430 ('install-dir=', 'd', "directory to install to"),
430 ('install-dir=', 'd', "directory to install to"),
431 ]
431 ]
432
432
433 def initialize_options(self):
433 def initialize_options(self):
434 self.install_dir = None
434 self.install_dir = None
435
435
436 def finalize_options(self):
436 def finalize_options(self):
437 self.set_undefined_options('symlink',
437 self.set_undefined_options('symlink',
438 ('install_lib', 'install_dir'),
438 ('install_lib', 'install_dir'),
439 )
439 )
440
440
441 def run(self):
441 def run(self):
442 if sys.platform == 'win32':
442 if sys.platform == 'win32':
443 raise Exception("This doesn't work on Windows.")
443 raise Exception("This doesn't work on Windows.")
444 pkg = os.path.join(os.getcwd(), 'IPython')
444 pkg = os.path.join(os.getcwd(), 'IPython')
445 dest = os.path.join(self.install_dir, 'IPython')
445 dest = os.path.join(self.install_dir, 'IPython')
446 if os.path.islink(dest):
446 if os.path.islink(dest):
447 print('removing existing symlink at %s' % dest)
447 print('removing existing symlink at %s' % dest)
448 os.unlink(dest)
448 os.unlink(dest)
449 print('symlinking %s -> %s' % (pkg, dest))
449 print('symlinking %s -> %s' % (pkg, dest))
450 os.symlink(pkg, dest)
450 os.symlink(pkg, dest)
451
451
452 class unsymlink(install):
452 class unsymlink(install):
453 def run(self):
453 def run(self):
454 dest = os.path.join(self.install_lib, 'IPython')
454 dest = os.path.join(self.install_lib, 'IPython')
455 if os.path.islink(dest):
455 if os.path.islink(dest):
456 print('removing symlink at %s' % dest)
456 print('removing symlink at %s' % dest)
457 os.unlink(dest)
457 os.unlink(dest)
458 else:
458 else:
459 print('No symlink exists at %s' % dest)
459 print('No symlink exists at %s' % dest)
460
460
461 class install_symlinked(install):
461 class install_symlinked(install):
462 def run(self):
462 def run(self):
463 if sys.platform == 'win32':
463 if sys.platform == 'win32':
464 raise Exception("This doesn't work on Windows.")
464 raise Exception("This doesn't work on Windows.")
465
465
466 # Run all sub-commands (at least those that need to be run)
466 # Run all sub-commands (at least those that need to be run)
467 for cmd_name in self.get_sub_commands():
467 for cmd_name in self.get_sub_commands():
468 self.run_command(cmd_name)
468 self.run_command(cmd_name)
469
469
470 # 'sub_commands': a list of commands this command might have to run to
470 # 'sub_commands': a list of commands this command might have to run to
471 # get its work done. See cmd.py for more info.
471 # get its work done. See cmd.py for more info.
472 sub_commands = [('install_lib_symlink', lambda self:True),
472 sub_commands = [('install_lib_symlink', lambda self:True),
473 ('install_scripts_sym', lambda self:True),
473 ('install_scripts_sym', lambda self:True),
474 ]
474 ]
475
475
476 class install_scripts_for_symlink(install_scripts):
476 class install_scripts_for_symlink(install_scripts):
477 """Redefined to get options from 'symlink' instead of 'install'.
477 """Redefined to get options from 'symlink' instead of 'install'.
478
478
479 I love distutils almost as much as I love setuptools.
479 I love distutils almost as much as I love setuptools.
480 """
480 """
481 def finalize_options(self):
481 def finalize_options(self):
482 self.set_undefined_options('build', ('build_scripts', 'build_dir'))
482 self.set_undefined_options('build', ('build_scripts', 'build_dir'))
483 self.set_undefined_options('symlink',
483 self.set_undefined_options('symlink',
484 ('install_scripts', 'install_dir'),
484 ('install_scripts', 'install_dir'),
485 ('force', 'force'),
485 ('force', 'force'),
486 ('skip_build', 'skip_build'),
486 ('skip_build', 'skip_build'),
487 )
487 )
488
488
489 #---------------------------------------------------------------------------
489 #---------------------------------------------------------------------------
490 # Verify all dependencies
490 # Verify all dependencies
491 #---------------------------------------------------------------------------
491 #---------------------------------------------------------------------------
492
492
493 def check_for_dependencies():
493 def check_for_dependencies():
494 """Check for IPython's dependencies.
494 """Check for IPython's dependencies.
495
495
496 This function should NOT be called if running under setuptools!
496 This function should NOT be called if running under setuptools!
497 """
497 """
498 from setupext.setupext import (
498 from setupext.setupext import (
499 print_line, print_raw, print_status,
499 print_line, print_raw, print_status,
500 check_for_sphinx, check_for_pygments,
500 check_for_sphinx, check_for_pygments,
501 check_for_nose, check_for_pexpect,
501 check_for_nose, check_for_pexpect,
502 check_for_pyzmq, check_for_readline,
502 check_for_pyzmq, check_for_readline,
503 check_for_jinja2, check_for_tornado
503 check_for_jinja2, check_for_tornado
504 )
504 )
505 print_line()
505 print_line()
506 print_raw("BUILDING IPYTHON")
506 print_raw("BUILDING IPYTHON")
507 print_status('python', sys.version)
507 print_status('python', sys.version)
508 print_status('platform', sys.platform)
508 print_status('platform', sys.platform)
509 if sys.platform == 'win32':
509 if sys.platform == 'win32':
510 print_status('Windows version', sys.getwindowsversion())
510 print_status('Windows version', sys.getwindowsversion())
511
511
512 print_raw("")
512 print_raw("")
513 print_raw("OPTIONAL DEPENDENCIES")
513 print_raw("OPTIONAL DEPENDENCIES")
514
514
515 check_for_sphinx()
515 check_for_sphinx()
516 check_for_pygments()
516 check_for_pygments()
517 check_for_nose()
517 check_for_nose()
518 if os.name == 'posix':
518 if os.name == 'posix':
519 check_for_pexpect()
519 check_for_pexpect()
520 check_for_pyzmq()
520 check_for_pyzmq()
521 check_for_tornado()
521 check_for_tornado()
522 check_for_readline()
522 check_for_readline()
523 check_for_jinja2()
523 check_for_jinja2()
524
524
525 #---------------------------------------------------------------------------
525 #---------------------------------------------------------------------------
526 # VCS related
526 # VCS related
527 #---------------------------------------------------------------------------
527 #---------------------------------------------------------------------------
528
528
529 # utils.submodule has checks for submodule status
529 # utils.submodule has checks for submodule status
530 execfile(pjoin('IPython','utils','submodule.py'), globals())
530 execfile(pjoin('IPython','utils','submodule.py'), globals())
531
531
532 class UpdateSubmodules(Command):
532 class UpdateSubmodules(Command):
533 """Update git submodules
533 """Update git submodules
534
534
535 IPython's external javascript dependencies live in a separate repo.
535 IPython's external javascript dependencies live in a separate repo.
536 """
536 """
537 description = "Update git submodules"
537 description = "Update git submodules"
538 user_options = []
538 user_options = []
539
539
540 def initialize_options(self):
540 def initialize_options(self):
541 pass
541 pass
542
542
543 def finalize_options(self):
543 def finalize_options(self):
544 pass
544 pass
545
545
546 def run(self):
546 def run(self):
547 failure = False
547 failure = False
548 try:
548 try:
549 self.spawn('git submodule init'.split())
549 self.spawn('git submodule init'.split())
550 self.spawn('git submodule update --recursive'.split())
550 self.spawn('git submodule update --recursive'.split())
551 except Exception as e:
551 except Exception as e:
552 failure = e
552 failure = e
553 print(e)
553 print(e)
554
554
555 if not check_submodule_status(repo_root) == 'clean':
555 if not check_submodule_status(repo_root) == 'clean':
556 print("submodules could not be checked out")
556 print("submodules could not be checked out")
557 sys.exit(1)
557 sys.exit(1)
558
558
559
559
560 def git_prebuild(pkg_dir, build_cmd=build_py):
560 def git_prebuild(pkg_dir, build_cmd=build_py):
561 """Return extended build or sdist command class for recording commit
561 """Return extended build or sdist command class for recording commit
562
562
563 records git commit in IPython.utils._sysinfo.commit
563 records git commit in IPython.utils._sysinfo.commit
564
564
565 for use in IPython.utils.sysinfo.sys_info() calls after installation.
565 for use in IPython.utils.sysinfo.sys_info() calls after installation.
566
566
567 Also ensures that submodules exist prior to running
567 Also ensures that submodules exist prior to running
568 """
568 """
569
569
570 class MyBuildPy(build_cmd):
570 class MyBuildPy(build_cmd):
571 ''' Subclass to write commit data into installation tree '''
571 ''' Subclass to write commit data into installation tree '''
572 def run(self):
572 def run(self):
573 build_cmd.run(self)
573 build_cmd.run(self)
574 # this one will only fire for build commands
574 # this one will only fire for build commands
575 if hasattr(self, 'build_lib'):
575 if hasattr(self, 'build_lib'):
576 self._record_commit(self.build_lib)
576 self._record_commit(self.build_lib)
577
577
578 def make_release_tree(self, base_dir, files):
578 def make_release_tree(self, base_dir, files):
579 # this one will fire for sdist
579 # this one will fire for sdist
580 build_cmd.make_release_tree(self, base_dir, files)
580 build_cmd.make_release_tree(self, base_dir, files)
581 self._record_commit(base_dir)
581 self._record_commit(base_dir)
582
582
583 def _record_commit(self, base_dir):
583 def _record_commit(self, base_dir):
584 import subprocess
584 import subprocess
585 proc = subprocess.Popen('git rev-parse --short HEAD',
585 proc = subprocess.Popen('git rev-parse --short HEAD',
586 stdout=subprocess.PIPE,
586 stdout=subprocess.PIPE,
587 stderr=subprocess.PIPE,
587 stderr=subprocess.PIPE,
588 shell=True)
588 shell=True)
589 repo_commit, _ = proc.communicate()
589 repo_commit, _ = proc.communicate()
590 repo_commit = repo_commit.strip().decode("ascii")
590 repo_commit = repo_commit.strip().decode("ascii")
591
591
592 out_pth = pjoin(base_dir, pkg_dir, 'utils', '_sysinfo.py')
592 out_pth = pjoin(base_dir, pkg_dir, 'utils', '_sysinfo.py')
593 if os.path.isfile(out_pth) and not repo_commit:
593 if os.path.isfile(out_pth) and not repo_commit:
594 # nothing to write, don't clobber
594 # nothing to write, don't clobber
595 return
595 return
596
596
597 print("writing git commit '%s' to %s" % (repo_commit, out_pth))
597 print("writing git commit '%s' to %s" % (repo_commit, out_pth))
598
598
599 # remove to avoid overwriting original via hard link
599 # remove to avoid overwriting original via hard link
600 try:
600 try:
601 os.remove(out_pth)
601 os.remove(out_pth)
602 except (IOError, OSError):
602 except (IOError, OSError):
603 pass
603 pass
604 with open(out_pth, 'w') as out_file:
604 with open(out_pth, 'w') as out_file:
605 out_file.writelines([
605 out_file.writelines([
606 '# GENERATED BY setup.py\n',
606 '# GENERATED BY setup.py\n',
607 'commit = u"%s"\n' % repo_commit,
607 'commit = u"%s"\n' % repo_commit,
608 ])
608 ])
609 return require_submodules(MyBuildPy)
609 return require_submodules(MyBuildPy)
610
610
611
611
612 def require_submodules(command):
612 def require_submodules(command):
613 """decorator for instructing a command to check for submodules before running"""
613 """decorator for instructing a command to check for submodules before running"""
614 class DecoratedCommand(command):
614 class DecoratedCommand(command):
615 def run(self):
615 def run(self):
616 if not check_submodule_status(repo_root) == 'clean':
616 if not check_submodule_status(repo_root) == 'clean':
617 print("submodules missing! Run `setup.py submodule` and try again")
617 print("submodules missing! Run `setup.py submodule` and try again")
618 sys.exit(1)
618 sys.exit(1)
619 command.run(self)
619 command.run(self)
620 return DecoratedCommand
620 return DecoratedCommand
621
621
622 #---------------------------------------------------------------------------
622 #---------------------------------------------------------------------------
623 # bdist related
623 # bdist related
624 #---------------------------------------------------------------------------
624 #---------------------------------------------------------------------------
625
625
626 def get_bdist_wheel():
626 def get_bdist_wheel():
627 """Construct bdist_wheel command for building wheels
627 """Construct bdist_wheel command for building wheels
628
628
629 Constructs py2-none-any tag, instead of py2.7-none-any
629 Constructs py2-none-any tag, instead of py2.7-none-any
630 """
630 """
631 class RequiresWheel(Command):
631 class RequiresWheel(Command):
632 description = "Dummy command for missing bdist_wheel"
632 description = "Dummy command for missing bdist_wheel"
633 user_options = []
633 user_options = []
634
634
635 def initialize_options(self):
635 def initialize_options(self):
636 pass
636 pass
637
637
638 def finalize_options(self):
638 def finalize_options(self):
639 pass
639 pass
640
640
641 def run(self):
641 def run(self):
642 print("bdist_wheel requires the wheel package")
642 print("bdist_wheel requires the wheel package")
643 sys.exit(1)
643 sys.exit(1)
644
644
645 if 'setuptools' not in sys.modules:
645 if 'setuptools' not in sys.modules:
646 return RequiresWheel
646 return RequiresWheel
647 else:
647 else:
648 try:
648 try:
649 from wheel.bdist_wheel import bdist_wheel, read_pkg_info, write_pkg_info
649 from wheel.bdist_wheel import bdist_wheel, read_pkg_info, write_pkg_info
650 except ImportError:
650 except ImportError:
651 return RequiresWheel
651 return RequiresWheel
652
652
653 class bdist_wheel_tag(bdist_wheel):
653 class bdist_wheel_tag(bdist_wheel):
654
654
655 def add_requirements(self, metadata_path):
655 def add_requirements(self, metadata_path):
656 """transform platform-dependent requirements"""
656 """transform platform-dependent requirements"""
657 pkg_info = read_pkg_info(metadata_path)
657 pkg_info = read_pkg_info(metadata_path)
658 # pkg_info is an email.Message object (?!)
658 # pkg_info is an email.Message object (?!)
659 # we have to remove the unconditional 'readline' and/or 'pyreadline' entries
659 # we have to remove the unconditional 'readline' and/or 'pyreadline' entries
660 # and transform them to conditionals
660 # and transform them to conditionals
661 requires = pkg_info.get_all('Requires-Dist')
661 requires = pkg_info.get_all('Requires-Dist')
662 del pkg_info['Requires-Dist']
662 del pkg_info['Requires-Dist']
663 def _remove_startswith(lis, prefix):
663 def _remove_startswith(lis, prefix):
664 """like list.remove, but with startswith instead of =="""
664 """like list.remove, but with startswith instead of =="""
665 found = False
665 found = False
666 for idx, item in enumerate(lis):
666 for idx, item in enumerate(lis):
667 if item.startswith(prefix):
667 if item.startswith(prefix):
668 found = True
668 found = True
669 break
669 break
670 if found:
670 if found:
671 lis.pop(idx)
671 lis.pop(idx)
672
672
673 for pkg in ("gnureadline", "pyreadline", "mock"):
673 for pkg in ("gnureadline", "pyreadline", "mock"):
674 _remove_startswith(requires, pkg)
674 _remove_startswith(requires, pkg)
675 requires.append("gnureadline; sys.platform == 'darwin' and platform.python_implementation == 'CPython'")
675 requires.append("gnureadline; sys.platform == 'darwin' and platform.python_implementation == 'CPython'")
676 requires.append("pyreadline (>=2.0); extra == 'terminal' and sys.platform == 'win32' and platform.python_implementation == 'CPython'")
676 requires.append("pyreadline (>=2.0); extra == 'terminal' and sys.platform == 'win32' and platform.python_implementation == 'CPython'")
677 requires.append("pyreadline (>=2.0); extra == 'all' and sys.platform == 'win32' and platform.python_implementation == 'CPython'")
677 requires.append("pyreadline (>=2.0); extra == 'all' and sys.platform == 'win32' and platform.python_implementation == 'CPython'")
678 requires.append("mock; extra == 'test' and python_version < '3.3'")
678 requires.append("mock; extra == 'test' and python_version < '3.3'")
679 for r in requires:
679 for r in requires:
680 pkg_info['Requires-Dist'] = r
680 pkg_info['Requires-Dist'] = r
681 write_pkg_info(metadata_path, pkg_info)
681 write_pkg_info(metadata_path, pkg_info)
682
682
683 return bdist_wheel_tag
683 return bdist_wheel_tag
684
684
685 #---------------------------------------------------------------------------
685 #---------------------------------------------------------------------------
686 # Notebook related
686 # Notebook related
687 #---------------------------------------------------------------------------
687 #---------------------------------------------------------------------------
688
688
689 class CompileCSS(Command):
689 class CompileCSS(Command):
690 """Recompile Notebook CSS
690 """Recompile Notebook CSS
691
691
692 Regenerate the compiled CSS from LESS sources.
692 Regenerate the compiled CSS from LESS sources.
693
693
694 Requires various dev dependencies, such as invoke and lessc.
694 Requires various dev dependencies, such as invoke and lessc.
695 """
695 """
696 description = "Recompile Notebook CSS"
696 description = "Recompile Notebook CSS"
697 user_options = [
697 user_options = [
698 ('minify', 'x', "minify CSS"),
698 ('minify', 'x', "minify CSS"),
699 ('force', 'f', "force recompilation of CSS"),
699 ('force', 'f', "force recompilation of CSS"),
700 ]
700 ]
701
701
702 def initialize_options(self):
702 def initialize_options(self):
703 self.minify = False
703 self.minify = False
704 self.force = False
704 self.force = False
705
705
706 def finalize_options(self):
706 def finalize_options(self):
707 self.minify = bool(self.minify)
707 self.minify = bool(self.minify)
708 self.force = bool(self.force)
708 self.force = bool(self.force)
709
709
710 def run(self):
710 def run(self):
711 cmd = ['invoke', 'css']
711 cmd = ['invoke', 'css']
712 if self.minify:
712 if self.minify:
713 cmd.append('--minify')
713 cmd.append('--minify')
714 if self.force:
714 if self.force:
715 cmd.append('--force')
715 cmd.append('--force')
716 try:
716 try:
717 p = Popen(cmd, cwd=pjoin(repo_root, "IPython", "html"), stderr=PIPE)
717 p = Popen(cmd, cwd=pjoin(repo_root, "IPython", "html"), stderr=PIPE)
718 except OSError:
718 except OSError:
719 raise DistutilsExecError("invoke is required to rebuild css (pip install invoke)")
719 raise DistutilsExecError("invoke is required to rebuild css (pip install invoke)")
720 out, err = p.communicate()
720 out, err = p.communicate()
721 if p.returncode:
721 if p.returncode:
722 if sys.version_info[0] >= 3:
722 if sys.version_info[0] >= 3:
723 err = err.decode('utf8', 'replace')
723 err = err.decode('utf8', 'replace')
724 raise DistutilsExecError(err.strip())
724 raise DistutilsExecError(err.strip())
725
725
726
726
727 class JavascriptVersion(Command):
727 class JavascriptVersion(Command):
728 """write the javascript version to notebook javascript"""
728 """write the javascript version to notebook javascript"""
729 description = "Write IPython version to javascript"
729 description = "Write IPython version to javascript"
730 user_options = []
730 user_options = []
731
731
732 def initialize_options(self):
732 def initialize_options(self):
733 pass
733 pass
734
734
735 def finalize_options(self):
735 def finalize_options(self):
736 pass
736 pass
737
737
738 def run(self):
738 def run(self):
739 nsfile = pjoin(repo_root, "IPython", "html", "static", "base", "js", "namespace.js")
739 nsfile = pjoin(repo_root, "IPython", "html", "static", "base", "js", "namespace.js")
740 with open(nsfile) as f:
740 with open(nsfile) as f:
741 lines = f.readlines()
741 lines = f.readlines()
742 with open(nsfile, 'w') as f:
742 with open(nsfile, 'w') as f:
743 for line in lines:
743 for line in lines:
744 if line.startswith("IPython.version"):
744 if line.startswith("IPython.version"):
745 line = 'IPython.version = "{0}";\n'.format(version)
745 line = 'IPython.version = "{0}";\n'.format(version)
746 f.write(line)
746 f.write(line)
747
747
748
748
749 def css_js_prerelease(command, strict=True):
749 def css_js_prerelease(command):
750 """decorator for building js/minified css prior to a release"""
750 """decorator for building js/minified css prior to a release"""
751 class DecoratedCommand(command):
751 class DecoratedCommand(command):
752 def run(self):
752 def run(self):
753 self.distribution.run_command('jsversion')
753 self.distribution.run_command('jsversion')
754 css = self.distribution.get_command_obj('css')
754 css = self.distribution.get_command_obj('css')
755 css.minify = True
755 css.minify = True
756 try:
756 try:
757 self.distribution.run_command('css')
757 self.distribution.run_command('css')
758 except Exception as e:
758 except Exception as e:
759 if strict:
759 log.warn("rebuilding css and sourcemaps failed (not a problem)")
760 raise
760 log.warn(str(e))
761 else:
762 log.warn("rebuilding css and sourcemaps failed (not a problem)")
763 log.warn(str(e))
764 command.run(self)
761 command.run(self)
765 return DecoratedCommand
762 return DecoratedCommand
General Comments 0
You need to be logged in to leave comments. Login now