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