~azzar1/unity/add-show-desktop-key

« back to all changes in this revision

Viewing changes to setup.py

  • Committer: drtomc
  • Date: 2007-12-05 21:24:33 UTC
  • Revision ID: svn-v3-trunk0:2b9c9e99-6f39-0410-b283-7f802c844ae2:trunk:5
Fix a bunch of glitches that Ivo and I found yesterday.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#!/usr/bin/env python
2
 
# IVLE - Informatics Virtual Learning Environment
3
 
# Copyright (C) 2007-2008 The University of Melbourne
4
 
#
5
 
# This program is free software; you can redistribute it and/or modify
6
 
# it under the terms of the GNU General Public License as published by
7
 
# the Free Software Foundation; either version 2 of the License, or
8
 
# (at your option) any later version.
9
 
#
10
 
# This program is distributed in the hope that it will be useful,
11
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
 
# GNU General Public License for more details.
14
 
#
15
 
# You should have received a copy of the GNU General Public License
16
 
# along with this program; if not, write to the Free Software
17
 
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
18
 
 
19
 
# Module: setup
20
 
# Author: Matt Giuca
21
 
# Date:   12/12/2007
22
 
 
23
 
# This is a command-line application, for use by the administrator.
24
 
# This program configures, builds and installs IVLE in three separate steps.
25
 
# It is called with at least one argument, which specifies which operation to
26
 
# take.
27
 
 
28
 
# setup.py listmake (for developer use only)
29
 
# Recurses through the source tree and builds a list of all files which should
30
 
# be copied upon installation. This should be run by the developer before
31
 
# cutting a distribution, and the listfile it generates should be included in
32
 
# the distribution, avoiding the administrator having to run it.
33
 
 
34
 
# setup.py conf [args]
35
 
# Configures IVLE with machine-specific details, most notably, various paths.
36
 
# Either prompts the administrator for these details or accepts them as
37
 
# command-line args.
38
 
# Creates www/conf/conf.py and trampoline/conf.h.
39
 
 
40
 
# setup.py build
41
 
# Compiles all files and sets up a jail template in the source directory.
42
 
# Details:
43
 
# Compiles (GCC) trampoline/trampoline.c to trampoline/trampoline.
44
 
# Creates jail/.
45
 
# Creates standard subdirs inside the jail, eg bin, opt, home, tmp.
46
 
# Copies console/ to a location within the jail.
47
 
# Copies OS programs and files to corresponding locations within the jail
48
 
#   (eg. python and Python libs, ld.so, etc).
49
 
# Generates .pyc files for all the IVLE .py files.
50
 
 
51
 
# setup.py install [--nojail] [--dry|n]
52
 
# (Requires root)
53
 
# Create target install directory ($target).
54
 
# Create $target/bin.
55
 
# Copy trampoline/trampoline to $target/bin.
56
 
# chown and chmod the installed trampoline.
57
 
# Copy www/ to $target.
58
 
# Copy jail/ to jails template directory (unless --nojail specified).
59
 
 
60
 
# TODO: List in help, and handle, args for the conf operation
61
 
 
62
 
import os
63
 
import stat
64
 
import shutil
65
 
import sys
66
 
import getopt
67
 
import string
68
 
import errno
69
 
import mimetypes
70
 
import compileall
71
 
 
72
 
# Try importing existing conf, but if we can't just set up defaults
73
 
# The reason for this is that these settings are used by other phases
74
 
# of setup besides conf, so we need to know them.
75
 
# Also this allows you to hit Return to accept the existing value.
76
 
try:
77
 
    confmodule = __import__("www/conf/conf")
78
 
    root_dir = confmodule.root_dir
79
 
    ivle_install_dir = confmodule.ivle_install_dir
80
 
    jail_base = confmodule.jail_base
81
 
except ImportError:
82
 
    # Just set reasonable defaults
83
 
    root_dir = "/ivle"
84
 
    ivle_install_dir = "/opt/ivle"
85
 
    jail_base = "/home/informatics/jails"
86
 
# Always defaults
87
 
allowed_uids = "0"
88
 
 
89
 
# Try importing install_list, but don't fail if we can't, because listmake can
90
 
# function without it.
91
 
try:
92
 
    import install_list
93
 
except:
94
 
    pass
95
 
 
96
 
# Mime types which will automatically be placed in the list by listmake.
97
 
# Note that listmake is not intended to be run by the final user (the system
98
 
# administrator who installs this), so the developers can customize the list
99
 
# as necessary, and include it in the distribution.
100
 
listmake_mimetypes = ['text/x-python', 'text/html',
101
 
    'application/x-javascript', 'application/javascript',
102
 
    'text/css', 'image/png']
103
 
 
104
 
# Main function skeleton from Guido van Rossum
105
 
# http://www.artima.com/weblogs/viewpost.jsp?thread=4829
106
 
 
107
 
class Usage(Exception):
108
 
    def __init__(self, msg):
109
 
        self.msg = msg
110
 
 
111
 
def main(argv=None):
112
 
    if argv is None:
113
 
        argv = sys.argv
114
 
 
115
 
    # Print the opening spiel including the GPL notice
116
 
 
117
 
    print """IVLE - Informatics Virtual Learning Environment Setup
118
 
Copyright (C) 2007-2008 The University of Melbourne
119
 
IVLE comes with ABSOLUTELY NO WARRANTY.
120
 
This is free software, and you are welcome to redistribute it
121
 
under certain conditions. See LICENSE.txt for details.
122
 
 
123
 
IVLE Setup
124
 
"""
125
 
 
126
 
    # First argument is the name of the setup operation
127
 
    try:
128
 
        operation = argv[1]
129
 
    except IndexError:
130
 
        # Print usage message and exit
131
 
        help([])
132
 
        return 1
133
 
 
134
 
    # Call the requested operation's function
135
 
    try:
136
 
        return {
137
 
            'help' : help,
138
 
            'conf' : conf,
139
 
            'build' : build,
140
 
            'listmake' : listmake,
141
 
            'install' : install,
142
 
        }[operation](argv[2:])
143
 
    except KeyError:
144
 
        print >>sys.stderr, (
145
 
            """Invalid operation '%s'. Try python setup.py help."""
146
 
            % operation)
147
 
 
148
 
    try:
149
 
        try:
150
 
            opts, args = getopt.getopt(argv[1:], "h", ["help"])
151
 
        except getopt.error, msg:
152
 
            raise Usage(msg)
153
 
        # more code, unchanged
154
 
    except Usage, err:
155
 
        print >>sys.stderr, err.msg
156
 
        print >>sys.stderr, "for help use --help"
157
 
        return 2
158
 
 
159
 
# Operation functions
160
 
 
161
 
def help(args):
162
 
    if args == []:
163
 
        print """Usage: python setup.py operation [args]
164
 
Operation (and args) can be:
165
 
    help [operation]
166
 
    conf [args]
167
 
    build
168
 
    install [--nojail] [-n|--dry]
169
 
"""
170
 
        return 1
171
 
    elif len(args) != 1:
172
 
        print """Usage: python setup.py help [operation]"""
173
 
        return 2
174
 
    else:
175
 
        operation = args[0]
176
 
 
177
 
    if operation == 'help':
178
 
        print """python setup.py help [operation]
179
 
Prints the usage message or detailed help on an operation, then exits."""
180
 
    elif operation == 'listmake':
181
 
        print """python setup.py listmake
182
 
(For developer use only)
183
 
Recurses through the source tree and builds a list of all files which should
184
 
be copied upon installation. This should be run by the developer before
185
 
cutting a distribution, and the listfile it generates should be included in
186
 
the distribution, avoiding the administrator having to run it."""
187
 
    elif operation == 'conf':
188
 
        print """python setup.py conf [args]
189
 
Configures IVLE with machine-specific details, most notably, various paths.
190
 
Either prompts the administrator for these details or accepts them as
191
 
command-line args.
192
 
Creates www/conf/conf.py and trampoline/conf.h.
193
 
Args are:
194
 
"""
195
 
    elif operation == 'build':
196
 
        print """python -O setup.py build [--dry|-n]
197
 
Compiles all files and sets up a jail template in the source directory.
198
 
-O is recommended to cause compilation to be optimised.
199
 
Details:
200
 
Compiles (GCC) trampoline/trampoline.c to trampoline/trampoline.
201
 
Creates jail/.
202
 
Creates standard subdirs inside the jail, eg bin, opt, home, tmp.
203
 
Copies console/ to a location within the jail.
204
 
Copies OS programs and files to corresponding locations within the jail
205
 
  (eg. python and Python libs, ld.so, etc).
206
 
Generates .pyc or .pyo files for all the IVLE .py files.
207
 
 
208
 
--dry | -n  Print out the actions but don't do anything."""
209
 
    elif operation == 'install':
210
 
        print """sudo python setup.py install [--nojail] [--dry|-n]
211
 
(Requires root)
212
 
Create target install directory ($target).
213
 
Create $target/bin.
214
 
Copy trampoline/trampoline to $target/bin.
215
 
chown and chmod the installed trampoline.
216
 
Copy www/ to $target.
217
 
Copy jail/ to jails template directory (unless --nojail specified).
218
 
 
219
 
--nojail    Do not copy the jail.
220
 
--dry | -n  Print out the actions but don't do anything."""
221
 
    else:
222
 
        print >>sys.stderr, (
223
 
            """Invalid operation '%s'. Try python setup.py help."""
224
 
            % operation)
225
 
    return 1
226
 
 
227
 
def listmake(args):
228
 
    # We build two separate lists, by walking www and console
229
 
    list_www = build_list_py_files('www')
230
 
    list_console = build_list_py_files('console')
231
 
    # Make sure that the files generated by conf are in the list
232
 
    # (since listmake is typically run before conf)
233
 
    if "www/conf/conf.py" not in list_www:
234
 
        list_www.append("www/conf/conf.py")
235
 
    # Make sure that console/python-console is in the list
236
 
    if "console/python-console" not in list_console:
237
 
        list_console.append("console/python-console")
238
 
    # Write these out to a file
239
 
    cwd = os.getcwd()
240
 
    # the files that will be created/overwritten
241
 
    listfile = os.path.join(cwd, "install_list.py")
242
 
 
243
 
    try:
244
 
        file = open(listfile, "w")
245
 
 
246
 
        file.write("""# IVLE Configuration File
247
 
# install_list.py
248
 
# Provides lists of all files to be installed by `setup.py install' from
249
 
# certain directories.
250
 
# Note that any files with the given filename plus 'c' or 'o' (that is,
251
 
# compiled .pyc or .pyo files) will be copied as well.
252
 
 
253
 
# List of all installable files in www directory.
254
 
list_www = """)
255
 
        writelist_pretty(file, list_www)
256
 
        file.write("""
257
 
# List of all installable files in console directory.
258
 
list_console = """)
259
 
        writelist_pretty(file, list_console)
260
 
 
261
 
        file.close()
262
 
    except IOError, (errno, strerror):
263
 
        print "IO error(%s): %s" % (errno, strerror)
264
 
        sys.exit(1)
265
 
 
266
 
    print "Successfully wrote install_list.py"
267
 
 
268
 
    print
269
 
    print ("You may modify the set of installable files before cutting the "
270
 
            "distribution:")
271
 
    print listfile
272
 
    print
273
 
 
274
 
    return 0
275
 
 
276
 
def build_list_py_files(dir):
277
 
    """Builds a list of all py files found in a directory and its
278
 
    subdirectories. Returns this as a list of strings."""
279
 
    pylist = []
280
 
    for (dirpath, dirnames, filenames) in os.walk(dir):
281
 
        # Exclude directories beginning with a '.' (such as '.svn')
282
 
        filter_mutate(lambda x: x[0] != '.', dirnames)
283
 
        # All *.py files are added to the list
284
 
        pylist += [os.path.join(dirpath, item) for item in filenames
285
 
            if mimetypes.guess_type(item)[0] in listmake_mimetypes]
286
 
    return pylist
287
 
 
288
 
def writelist_pretty(file, list):
289
 
    """Writes a list one element per line, to a file."""
290
 
    if list == []:
291
 
        file.write("[]\n")
292
 
    else:
293
 
        file.write('[\n')
294
 
        for elem in list:
295
 
            file.write('    %s,\n' % repr(elem))
296
 
        file.write(']\n')
297
 
 
298
 
def conf(args):
299
 
    global root_dir, ivle_install_dir, jail_base, allowed_uids
300
 
    # Set up some variables
301
 
 
302
 
    cwd = os.getcwd()
303
 
    # the files that will be created/overwritten
304
 
    conffile = os.path.join(cwd, "www/conf/conf.py")
305
 
    conf_hfile = os.path.join(cwd, "trampoline/conf.h")
306
 
 
307
 
    # Fixed config options that we don't ask the admin
308
 
 
309
 
    default_app = "dummy"
310
 
 
311
 
    print """This tool will create the following files:
312
 
    %s
313
 
    %s
314
 
prompting you for details about your configuration. The file will be
315
 
overwritten if it already exists. It will *not* install or deploy IVLE.
316
 
 
317
 
Please hit Ctrl+C now if you do not wish to do this.
318
 
""" % (conffile, conf_hfile)
319
 
 
320
 
    # Get information from the administrator
321
 
    # If EOF is encountered at any time during the questioning, just exit
322
 
    # silently
323
 
 
324
 
    root_dir = query_user(root_dir,
325
 
    """Root directory where IVLE is located (in URL space):""")
326
 
    ivle_install_dir = query_user(ivle_install_dir,
327
 
    'Root directory where IVLE will be installed (on the local file '
328
 
    'system):')
329
 
    jail_base = query_user(jail_base,
330
 
    """Root directory where the jails (containing user files) are stored
331
 
(on the local file system):""")
332
 
    allowed_uids = query_user(allowed_uids,
333
 
    """UID of the web server process which will run IVLE.
334
 
Only this user may execute the trampoline. May specify multiple users as
335
 
a comma-separated list.
336
 
    (eg. "1002,78")""")
337
 
 
338
 
    # Error handling on input values
339
 
 
340
 
    try:
341
 
        allowed_uids = map(int, allowed_uids.split(','))
342
 
    except ValueError:
343
 
        print >>sys.stderr, (
344
 
        "Invalid UID list (%s).\n"
345
 
        "Must be a comma-separated list of integers." % allowed_uids)
346
 
        return 1
347
 
 
348
 
    # Write www/conf/conf.py
349
 
 
350
 
    try:
351
 
        conf = open(conffile, "w")
352
 
 
353
 
        conf.write("""# IVLE Configuration File
354
 
# conf.py
355
 
# Miscellaneous application settings
356
 
 
357
 
 
358
 
# In URL space, where in the site is IVLE located. (All URLs will be prefixed
359
 
# with this).
360
 
# eg. "/" or "/ivle".
361
 
root_dir = "%s"
362
 
 
363
 
# In the local file system, where IVLE is actually installed.
364
 
# This directory should contain the "www" and "bin" directories.
365
 
ivle_install_dir = "%s"
366
 
 
367
 
# In the local file system, where are the student/user file spaces located.
368
 
# The user jails are expected to be located immediately in subdirectories of
369
 
# this location.
370
 
jail_base = "%s"
371
 
 
372
 
# Which application to load by default (if the user navigates to the top level
373
 
# of the site). This is the app's URL name.
374
 
# Note that if this app requires authentication, the user will first be
375
 
# presented with the login screen.
376
 
default_app = "%s"
377
 
""" % (root_dir, ivle_install_dir, jail_base, default_app))
378
 
 
379
 
        conf.close()
380
 
    except IOError, (errno, strerror):
381
 
        print "IO error(%s): %s" % (errno, strerror)
382
 
        sys.exit(1)
383
 
 
384
 
    print "Successfully wrote www/conf/conf.py"
385
 
 
386
 
    # Write trampoline/conf.h
387
 
 
388
 
    try:
389
 
        conf = open(conf_hfile, "w")
390
 
 
391
 
        conf.write("""/* IVLE Configuration File
392
 
 * conf.h
393
 
 * Administrator settings required by trampoline.
394
 
 * Note: trampoline will have to be rebuilt in order for changes to this file
395
 
 * to take effect.
396
 
 */
397
 
 
398
 
/* In the local file system, where are the jails located.
399
 
 * The trampoline does not allow the creation of a jail anywhere besides
400
 
 * jail_base or a subdirectory of jail_base.
401
 
 */
402
 
static const char* jail_base = "%s";
403
 
 
404
 
/* Which user IDs are allowed to run the trampoline.
405
 
 * This list should be limited to the web server user.
406
 
 * (Note that root is an implicit member of this list).
407
 
 */
408
 
static const int allowed_uids[] = { %s };
409
 
""" % (jail_base, repr(allowed_uids)[1:-1]))
410
 
 
411
 
        conf.close()
412
 
    except IOError, (errno, strerror):
413
 
        print "IO error(%s): %s" % (errno, strerror)
414
 
        sys.exit(1)
415
 
 
416
 
    print "Successfully wrote trampoline/conf.h"
417
 
 
418
 
    print
419
 
    print "You may modify the configuration at any time by editing"
420
 
    print conffile
421
 
    print conf_hfile
422
 
    print
423
 
    return 0
424
 
 
425
 
def build(args):
426
 
    dry = False     # Set to True later if --dry
427
 
 
428
 
    # Compile the trampoline
429
 
    action_runprog('gcc', ['-Wall', '-o', 'trampoline/trampoline',
430
 
        'trampoline/trampoline.c'], dry)
431
 
 
432
 
    # Create the jail and its subdirectories
433
 
    action_mkdir('jail', dry)
434
 
    action_mkdir('jail/bin', dry)
435
 
    action_mkdir('jail/lib', dry)
436
 
    action_mkdir('jail/usr/bin', dry)
437
 
    action_mkdir('jail/usr/lib', dry)
438
 
    action_mkdir('jail/opt/ivle', dry)
439
 
    action_mkdir('jail/home', dry)
440
 
    action_mkdir('jail/tmp', dry)
441
 
 
442
 
    # Copy all console files into the jail
443
 
    action_copylist(install_list.list_console, 'jail/opt/ivle', dry)
444
 
 
445
 
    # TODO: Copy operating system files into the jail
446
 
 
447
 
    # Compile .py files into .pyc or .pyo files
448
 
    compileall.compile_dir('www', quiet=True)
449
 
    compileall.compile_dir('console', quiet=True)
450
 
 
451
 
    return 0
452
 
 
453
 
def install(args):
454
 
    # Create the target directory
455
 
    nojail = False  # Set to True later if --nojail
456
 
    dry = False     # Set to True later if --dry
457
 
 
458
 
    if not dry and os.geteuid() != 0:
459
 
        print >>sys.stderr, "Must be root to run install"
460
 
        print >>sys.stderr, "(I need to chown some files)."
461
 
        return 1
462
 
 
463
 
    # Create the target (install) directory
464
 
    action_mkdir(ivle_install_dir, dry)
465
 
 
466
 
    # Create bin and copy the compiled files there
467
 
    action_mkdir(os.path.join(ivle_install_dir, 'bin'), dry)
468
 
    tramppath = os.path.join(ivle_install_dir, 'bin/trampoline')
469
 
    action_copyfile('trampoline/trampoline', tramppath, dry)
470
 
    # chown trampoline to root and set setuid bit
471
 
    action_chown_setuid(tramppath, dry)
472
 
 
473
 
    # Copy the www directory using the list
474
 
    action_copylist(install_list.list_www, ivle_install_dir, dry)
475
 
 
476
 
    if not nojail:
477
 
        # Copy the local jail directory built by the build action
478
 
        # to the jails template directory (it will be used as a template
479
 
        # for all the students' jails).
480
 
        action_copytree('jail', os.path.join(jail_base, 'template'), dry)
481
 
 
482
 
    return 0
483
 
 
484
 
# The actions call Python os functions but print actions and handle dryness.
485
 
# May still throw os exceptions if errors occur.
486
 
 
487
 
class RunError:
488
 
    """Represents an error when running a program (nonzero return)."""
489
 
    def __init__(self, prog, retcode):
490
 
        self.prog = prog
491
 
        self.retcode = retcode
492
 
    def __str__(self):
493
 
        return str(self.prog) + " returned " + repr(self.retcode)
494
 
 
495
 
def action_runprog(prog, args, dry):
496
 
    """Runs a unix program. Searches in $PATH. Synchronous (waits for the
497
 
    program to return). Runs in the current environment. First prints the
498
 
    action as a "bash" line.
499
 
 
500
 
    Throws a RunError with a retcode of the return value of the program,
501
 
    if the program did not return 0.
502
 
 
503
 
    prog: String. Name of the program. (No path required, if in $PATH).
504
 
    args: [String]. Arguments to the program.
505
 
    dry: Bool. If True, prints but does not execute.
506
 
    """
507
 
    print prog, string.join(args, ' ')
508
 
    if dry: return
509
 
    ret = os.spawnvp(os.P_WAIT, prog, args)
510
 
    if ret != 0:
511
 
        raise RunError(prog, ret)
512
 
 
513
 
def action_mkdir(path, dry):
514
 
    """Calls mkdir. Silently ignored if the directory already exists.
515
 
    Creates all parent directories as necessary."""
516
 
    print "mkdir -p", path
517
 
    if dry: return
518
 
    try:
519
 
        os.makedirs(path)
520
 
    except OSError, (err, msg):
521
 
        if err != errno.EEXIST:
522
 
            raise
523
 
 
524
 
def action_copytree(src, dst, dry):
525
 
    """Copies an entire directory tree. Symlinks are seen as normal files and
526
 
    copies of the entire file (not the link) are made. Creates all parent
527
 
    directories as necessary.
528
 
 
529
 
    See shutil.copytree."""
530
 
    if os.access(dst, os.F_OK):
531
 
        print "rm -r", dst
532
 
        if not dry:
533
 
            shutil.rmtree(dst, True)
534
 
    print "cp -r", src, dst
535
 
    if dry: return
536
 
    shutil.copytree(src, dst)
537
 
 
538
 
def action_copylist(srclist, dst, dry):
539
 
    """Copies all files in a list to a new location. The files in the list
540
 
    are read relative to the current directory, and their destinations are the
541
 
    same paths relative to dst. Creates all parent directories as necessary.
542
 
    """
543
 
    for srcfile in srclist:
544
 
        dstfile = os.path.join(dst, srcfile)
545
 
        dstdir = os.path.split(dstfile)[0]
546
 
        if not os.path.isdir(dstdir):
547
 
            action_mkdir(dstdir, dry)
548
 
        print "cp -f", srcfile, dstfile
549
 
        if not dry:
550
 
            shutil.copyfile(srcfile, dstfile)
551
 
 
552
 
def action_copyfile(src, dst, dry):
553
 
    """Copies one file to a new location. Creates all parent directories
554
 
    as necessary.
555
 
    """
556
 
    dstdir = os.path.split(dst)[0]
557
 
    if not os.path.isdir(dstdir):
558
 
        action_mkdir(dstdir, dry)
559
 
    print "cp -f", src, dst
560
 
    if not dry:
561
 
        shutil.copyfile(src, dst)
562
 
 
563
 
def action_chown_setuid(file, dry):
564
 
    """Chowns a file to root, and sets the setuid bit on the file.
565
 
    Calling this function requires the euid to be root.
566
 
    The actual mode of path is set to: rws--s--s
567
 
    """
568
 
    print "chown root:root", file
569
 
    if not dry:
570
 
        os.chown(file, 0, 0)
571
 
    print "chmod a+xs", file
572
 
    print "chmod u+rw", file
573
 
    if not dry:
574
 
        os.chmod(file, stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
575
 
            | stat.S_ISUID | stat.S_IRUSR | stat.S_IWUSR)
576
 
 
577
 
def query_user(default, prompt):
578
 
    """Prompts the user for a string, which is read from a line of stdin.
579
 
    Exits silently if EOF is encountered. Returns the string, with spaces
580
 
    removed from the beginning and end.
581
 
 
582
 
    Returns default if a 0-length line (after spaces removed) was read.
583
 
    """
584
 
    sys.stdout.write('%s\n    (default: "%s")\n>' % (prompt, default))
585
 
    try:
586
 
        val = sys.stdin.readline()
587
 
    except KeyboardInterrupt:
588
 
        # Ctrl+C
589
 
        sys.stdout.write("\n")
590
 
        sys.exit(1)
591
 
    sys.stdout.write("\n")
592
 
    # If EOF, exit
593
 
    if val == '': sys.exit(1)
594
 
    # If empty line, return default
595
 
    val = val.strip()
596
 
    if val == '': return default
597
 
    return val
598
 
 
599
 
def filter_mutate(function, list):
600
 
    """Like built-in filter, but mutates the given list instead of returning a
601
 
    new one. Returns None."""
602
 
    i = len(list)-1
603
 
    while i >= 0:
604
 
        # Delete elements which do not match
605
 
        if not function(list[i]):
606
 
            del list[i]
607
 
        i -= 1
608
 
 
609
 
if __name__ == "__main__":
610
 
    sys.exit(main())