root / pykota / trunk / bin / edpykota @ 1179

Revision 1179, 26.1 kB (checked in by jalet, 20 years ago)

Bug fix wrt no user/group name command line argument to edpykota

  • Property svn:eol-style set to native
  • Property svn:executable set to *
  • Property svn:keywords set to Author Date Id Revision
Line 
1#! /usr/bin/env python
2# -*- coding: ISO-8859-15 -*-
3
4# PyKota Print Quota Editor
5#
6# PyKota - Print Quotas for CUPS and LPRng
7#
8# (c) 2003 Jerome Alet <alet@librelogiciel.com>
9# This program is free software; you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation; either version 2 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program; if not, write to the Free Software
21# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
22#
23# $Id$
24#
25# $Log$
26# Revision 1.61  2003/11/12 13:06:35  jalet
27# Bug fix wrt no user/group name command line argument to edpykota
28#
29# Revision 1.60  2003/10/09 21:25:24  jalet
30# Multiple printer names or wildcards can be passed on the command line
31# separated with commas.
32# Beta phase.
33#
34# Revision 1.59  2003/10/07 09:07:27  jalet
35# Character encoding added to please latest version of Python
36#
37# Revision 1.58  2003/10/03 12:27:01  jalet
38# Several optimizations, especially with LDAP backend
39#
40# Revision 1.57  2003/08/20 16:01:19  jalet
41# Comment added.
42#
43# Revision 1.56  2003/07/29 20:55:17  jalet
44# 1.14 is out !
45#
46# Revision 1.55  2003/07/28 09:11:12  jalet
47# PyKota now tries to add its attributes intelligently in existing LDAP
48# directories.
49#
50# Revision 1.54  2003/07/21 06:32:42  jalet
51# Prevents email messages to be sent at modification/creation time for
52# a user/group quota
53#
54# Revision 1.53  2003/07/09 06:03:41  jalet
55# Fixed typo when using edpykota --prototype
56#
57# Revision 1.52  2003/07/07 12:11:13  jalet
58# Small fix
59#
60# Revision 1.51  2003/07/07 11:55:50  jalet
61# Small fix
62#
63# Revision 1.50  2003/07/05 12:33:53  jalet
64# More on previous fix.
65#
66# Revision 1.49  2003/07/05 12:32:07  jalet
67# Ensure that the user don't pass more than two prices for a printer.
68#
69# Revision 1.48  2003/06/25 19:52:30  jalet
70# Should be ready for testing :-)
71#
72# Revision 1.47  2003/06/25 14:10:01  jalet
73# Hey, it may work (edpykota --reset excepted) !
74#
75# Revision 1.46  2003/06/16 11:59:09  jalet
76# More work on LDAP
77#
78# Revision 1.45  2003/06/11 19:32:00  jalet
79# Severe bug wrt account balance setting should be corrected.
80#
81# Revision 1.44  2003/04/29 22:03:38  jalet
82# Better error handling.
83#
84# Revision 1.43  2003/04/23 22:13:56  jalet
85# Preliminary support for LPRng added BUT STILL UNTESTED.
86#
87# Revision 1.42  2003/04/17 13:38:47  jalet
88# Docstring corrected for better manual page
89#
90# Revision 1.41  2003/04/16 12:35:49  jalet
91# Groups quota work now !
92#
93# Revision 1.40  2003/04/16 08:22:09  jalet
94# More strict error detection.
95# Minor code rewrite to avoid some repetitive tests.
96#
97# Revision 1.39  2003/04/16 08:01:53  jalet
98# edpykota --charge command line option works now.
99#
100# Revision 1.38  2003/04/15 22:02:43  jalet
101# More complete docstring
102#
103# Revision 1.37  2003/04/15 21:58:33  jalet
104# edpykota now accepts a --delete option.
105# Preparation to allow edpykota to accept much more command line options
106# (WARNING : docstring is OK, but code isn't !)
107#
108# Revision 1.36  2003/04/15 13:55:28  jalet
109# Options --limitby and --balance added to edpykota
110#
111# Revision 1.35  2003/04/15 13:06:39  jalet
112# Allow to add a printer without any user
113#
114# Revision 1.34  2003/04/11 16:51:11  jalet
115# Bug fix for edpykota --add with users who already had a quota on the printer.
116#
117# Revision 1.33  2003/04/10 21:47:20  jalet
118# Job history added. Upgrade script neutralized for now !
119#
120# Revision 1.32  2003/04/08 21:31:39  jalet
121# (anything or 0) = anything !!! Go back to school Jerome !
122#
123# Revision 1.31  2003/04/08 21:13:44  jalet
124# Prepare --groups option to work.
125#
126# Revision 1.30  2003/04/08 21:10:18  jalet
127# Checks --groups option presence instead of --users because --users is the default.
128#
129# Revision 1.29  2003/04/05 09:28:56  jalet
130# Unnecessary message was logged
131#
132# Revision 1.28  2003/03/29 13:45:26  jalet
133# GPL paragraphs were incorrectly (from memory) copied into the sources.
134# Two README files were added.
135# Upgrade script for PostgreSQL pre 1.01 schema was added.
136#
137# Revision 1.27  2003/03/10 00:23:04  jalet
138# Bad english
139#
140# Revision 1.26  2003/03/10 00:11:27  jalet
141# Cleaner example.
142#
143# Revision 1.25  2003/03/09 23:56:21  jalet
144# Option noquota added to do accounting only.
145#
146# Revision 1.24  2003/02/27 23:48:41  jalet
147# Correctly maps PyKota's log levels to syslog log levels
148#
149# Revision 1.23  2003/02/27 22:55:20  jalet
150# WARN log priority doesn't exist.
151#
152# Revision 1.22  2003/02/27 09:37:02  jalet
153# Wildcards seem to work now
154#
155# Revision 1.21  2003/02/27 09:04:46  jalet
156# user and group names can be passed as wildcards if the --add option
157# is not set. The default is to act on all users or groups.
158#
159# Revision 1.20  2003/02/10 12:07:30  jalet
160# Now repykota should output the recorded total page number for each printer too.
161#
162# Revision 1.19  2003/02/09 13:40:29  jalet
163# typo
164#
165# Revision 1.18  2003/02/09 12:56:53  jalet
166# Internationalization begins...
167#
168# Revision 1.17  2003/02/08 22:47:23  jalet
169# Option --reset can now be used without having to use soft and hard limits
170# on the command line.
171#
172# Revision 1.16  2003/02/08 22:39:46  jalet
173# --reset command line option added
174#
175# Revision 1.15  2003/02/08 22:20:01  jalet
176# Clarification on why we don't check with /etc/passwd to see if the user
177# name is valid or not.
178#
179# Revision 1.14  2003/02/08 22:18:15  jalet
180# Now checks user and group names for validity before adding them
181#
182# Revision 1.13  2003/02/08 22:09:02  jalet
183# Only printer was added the first time.
184#
185# Revision 1.12  2003/02/08 21:44:49  jalet
186# Python 2.1 string module doesn't define ascii_letters
187#
188# Revision 1.11  2003/02/08 09:42:44  jalet
189# Better handle wrong or bad command line arguments
190#
191# Revision 1.10  2003/02/08 09:39:20  jalet
192# typos
193#
194# Revision 1.9  2003/02/08 09:38:06  jalet
195# Badly placed test
196#
197# Revision 1.8  2003/02/07 22:53:57  jalet
198# Checks if printer name is valid before adding it
199#
200# Revision 1.7  2003/02/07 22:17:58  jalet
201# Incomplete test
202#
203# Revision 1.6  2003/02/07 22:13:13  jalet
204# Perhaps edpykota is now able to add printers !!! Oh, stupid me !
205#
206# Revision 1.5  2003/02/06 14:49:04  jalet
207# edpykota should be ok now
208#
209# Revision 1.4  2003/02/06 14:28:59  jalet
210# edpykota should be ok, minus some typos
211#
212# Revision 1.3  2003/02/06 10:47:21  jalet
213# Documentation string and command line options didn't match.
214#
215# Revision 1.2  2003/02/06 10:39:23  jalet
216# Preliminary edpykota work.
217#
218# Revision 1.1  2003/02/05 21:41:09  jalet
219# Skeletons added for all command line tools
220#
221#
222#
223
224import sys
225
226from pykota import version
227from pykota.tool import PyKotaTool, PyKotaToolError
228from pykota.config import PyKotaConfigError
229from pykota.storage import PyKotaStorageError
230
231__doc__ = """edpykota v%s (C) 2003 C@LL - Conseil Internet & Logiciels Libres
232A Print Quota editor for PyKota.
233
234command line usage :
235
236  edpykota [options] user1 user2 ... userN
237 
238  edpykota [options] group1 group2 ... groupN
239
240options :
241
242  -v | --version       Prints edpykota's version number then exits.
243  -h | --help          Prints this message then exits.
244 
245  -a | --add           Adds users and/or printers if they don't
246                       exist on the Quota Storage Server.
247                       
248  -d | --delete        Deletes users/groups from the quota storage.
249                       Printers are never deleted.
250                       
251  -c | --charge p[,j]  Sets the price per page and per job to charge
252                       for a particular printer. Job price is optional.
253                       If both are to be set, separate them with a comma.
254                       Floating point values are allowed.
255 
256  -i | --ingroups g1[,g2...]  Puts the users into each of the groups
257                              listed, separated by commas. The groups
258                              must already exist in the Quota Storage.
259 
260  -u | --users         Edit users print quotas, this is the default.
261 
262  -P | --printer p     Edit quotas on printer p only. Actually p can
263                       use wildcards characters to select only
264                       some printers. The default value is *, meaning
265                       all printers.
266                       You can specify several names or wildcards,
267                       by separating them with commas.
268 
269  -g | --groups        Edit groups print quotas instead of users.
270                         
271  -p | --prototype u|g Uses user u or group g as a prototype to set
272                       print quotas
273                       
274  -n | --noquota       Doesn't set a quota but only does accounting.
275 
276  -r | --reset         Resets the printed page counter for the user
277                       or group to zero. The life time page counter
278                       is kept unchanged.
279                       
280  -l | --limitby l     Choose if the user/group is limited in printing                     
281                       by its account balance or by its page quota.
282                       The default value is 'quota'. Allowed values
283                       are 'quota' and 'balance'.
284                       
285  -b | --balance b     Sets the user's account balance to b.                     
286                       Account balance may be increase or decreased
287                       if b is prefixed with + or -.
288                       WARNING : when decreasing account balance,
289                       the total paid so far by the user is decreased
290                       too.
291                       Groups don't have a real balance, but the
292                       sum of their users' account balance.
293                       
294  -S | --softlimit sl  Sets the quota soft limit to sl pages.                       
295 
296  -H | --hardlimit hl  Sets the quota hard limit to hl pages.
297 
298  user1 through userN and group1 through groupN can use wildcards
299  if the --add option is not set.
300 
301examples :                             
302
303  $ edpykota -p jerome john paul george ringo
304 
305  This will set print quotas for the users john, paul, george and ringo
306  to the same values than user jerome. User jerome must exist.
307 
308  $ edpykota --printer lp -S 50 -H 60 jerome
309 
310  This will set jerome's print quota on the lp printer to a soft limit
311  of 50 pages, and a hard limit of 60 pages. If either user jerome or
312  printer lp doesn't exist on the Quota Storage Server then nothing is done.
313
314  $ edpykota --add --printer lp --ingroups coders,it -S 50 -H 60 jerome
315 
316  Same as above, but if either user jerome or printer lp doesn't exist
317  on the Quota Storage Server they are automatically added. Also
318  user jerome is put into the groups "coders" and "it" which must
319  already exist in the Quota Storage.
320  WARNING : the CUPS PPD file for this printer or the /etc/printcap
321            file (depending on your printing backend)  must still be
322            modified manually, as well as pykota's configuration file
323            for a new printer to be managed successfully.
324           
325  $ edpykota -g -S 500 -H 550 financial support           
326 
327  This will set print quota soft limit to 500 pages and hard limit
328  to 550 pages for groups financial and support on all printers.
329 
330  $ edpykota --reset jerome "jo*"
331 
332  This will reset jerome's page counter to zero on all printers, as
333  well as every user whose name begins with 'jo'.
334  Their life time page counter on each printer will be kept unchanged.
335 
336  $ edpykota --printer hpcolor --noquota jerome
337 
338  This will tell PyKota to not limit jerome when printing on the
339  hpcolor printer. All his jobs will be allowed on this printer, but
340  accounting of the pages he prints will still be kept.
341  Print Quotas for jerome on other printers are unchanged.
342 
343  $ edpykota --limitby balance jerome
344 
345  This will tell PyKota to limit jerome by his account's balance
346  when printing.
347 
348  $ edpykota --balance +10.0 jerome
349 
350  This will increase jerome's account balance by 10.0 (in your
351  own currency). You can decrease the account balance with a
352  dash prefix, and set it to a fixed amount with no prefix.
353 
354  $ edpykota --delete jerome rachel
355 
356  This will completely delete jerome and rachel from the Quota Storage
357  database. All their quotas and jobs will be deleted too.
358 
359  $ edpykota --printer lp --charge 0.1
360 
361  This will set the page price for printer lp to 0.1. Job price
362  will not be changed.
363
364This program is free software; you can redistribute it and/or modify
365it under the terms of the GNU General Public License as published by
366the Free Software Foundation; either version 2 of the License, or
367(at your option) any later version.
368
369This program is distributed in the hope that it will be useful,
370but WITHOUT ANY WARRANTY; without even the implied warranty of
371MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
372GNU General Public License for more details.
373
374You should have received a copy of the GNU General Public License
375along with this program; if not, write to the Free Software
376Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
377
378Please e-mail bugs to: %s""" % (version.__version__, version.__author__)
379       
380class EdPyKota(PyKotaTool) :       
381    """A class for edpykota."""
382    def main(self, names, options) :
383        """Edit user or group quotas."""
384       
385        suffix = (options["groups"] and "Group") or "User"       
386       
387        softlimit = hardlimit = None   
388        if not options["noquota"] :
389            if options["softlimit"] :
390                try :
391                    softlimit = int(options["softlimit"].strip())
392                except ValueError :   
393                    raise PyKotaToolError, _("Invalid softlimit value %s.") % options["softlimit"]
394            if options["hardlimit"] :
395                try :
396                    hardlimit = int(options["hardlimit"].strip())
397                except ValueError :   
398                    raise PyKotaToolError, _("Invalid hardlimit value %s.") % options["hardlimit"]
399            if (softlimit is not None) and (hardlimit is not None) and (hardlimit < softlimit) :       
400                # error, exchange them
401                self.logger.log_message(_("Hard limit %i is less than soft limit %i, values will be exchanged.") % (hardlimit, softlimit))
402                (softlimit, hardlimit) = (hardlimit, softlimit)
403           
404        balance = options["balance"]
405        if balance :
406            balance = balance.strip()
407            try :
408                balancevalue = float(balance)
409            except ValueError :   
410                raise PyKotaToolError, _("Invalid balance value %s" % options["balance"])
411           
412        if options["charge"] :
413            try :
414                charges = [float(part) for part in options["charge"].split(',', 1)]
415            except ValueError :   
416                raise PyKotaToolError, _("Invalid charge amount value %s" % options["charge"])
417            else :   
418                if len(charges) > 2 :
419                    charges = charges[:2]
420                if len(charges) != 2 :
421                    charges = [charges[0], None]
422                   
423        limitby = options["limitby"]
424        if limitby :
425            limitby = limitby.strip().lower()
426        if limitby and (limitby not in ('quota', 'balance')) :   
427            raise PyKotaToolError, _("Invalid limitby value %s" % options["limitby"])
428           
429        if options["ingroups"] :   
430            groupnames = [gname.strip() for gname in options["ingroups"].split(',')]
431        else :   
432            groupnames = []
433           
434        if options["prototype"] :   
435            protoentry = getattr(self.storage, "get%s" % suffix)(options["prototype"])
436           
437        printeradded = 0
438        printers = self.storage.getMatchingPrinters(options["printer"])
439        if not printers :
440            pname = options["printer"]
441            if options["add"] and pname :
442                if self.isValidName(pname) :
443                    printers = [ self.storage.addPrinter(pname) ]
444                    if printers[0].Exists :
445                        printeradded = 1
446                    else :   
447                        raise PyKotaToolError, _("Impossible to add printer %s") % pname
448                else :   
449                    raise PyKotaToolError, _("Invalid printer name %s") % pname
450            else :
451                raise PyKotaToolError, _("There's no printer matching %s") % pname
452        if not names :   
453            if options["add"] :
454                if not printeradded :
455                    raise PyKotaToolError, _("You have to pass user or group names on the command line")
456                else :   
457                    names = getattr(self.storage, "getAll%ssNames" % suffix)()
458            else :   
459                names = [ "*" ] # all users
460               
461        changed = {} # tracks changes made at the user/group level
462        for printer in printers :
463            if options["charge"] :
464                (perpage, perjob) = charges
465                printer.setPrices(perpage, perjob)   
466               
467            if options["prototype"] :
468                if protoentry.Exists :
469                    protoquota = getattr(self.storage, "get%sPQuota" % suffix)(protoentry, printer)
470                    if not protoquota.Exists :
471                        self.logger.log_message(_("Prototype %s not found in Quota Storage for printer %s.") % (protoentry.Name, printer.Name))
472                        continue    # skip this printer
473                    else :   
474                        (softlimit, hardlimit) = (protoquota.SoftLimit, protoquota.HardLimit)
475                else :       
476                    self.logger.log_message(_("Prototype object %s not found in Quota Storage.") % protoentry.Name)
477                   
478            if not options["noquota"] :   
479                if hardlimit is None :   
480                    hardlimit = softlimit
481                    if hardlimit is not None :
482                        self.logger.log_message(_("Undefined hard limit set to soft limit (%s) on printer %s.") % (str(hardlimit), printer.Name))
483                if softlimit is None :   
484                    softlimit = hardlimit
485                    if softlimit is not None :
486                        self.logger.log_message(_("Undefined soft limit set to hard limit (%s) on printer %s.") % (str(softlimit), printer.Name))
487                       
488            if options["add"] :   
489                allentries = []   
490                for name in names :
491                    entry = getattr(self.storage, "get%s" % suffix)(name)
492                    entrypquota = getattr(self.storage, "get%sPQuota" % suffix)(entry, printer)
493                    allentries.append((entry, entrypquota))
494            else :   
495                allentries = getattr(self.storage, "getPrinter%ssAndQuotas" % suffix)(printer, names)
496               
497            for (entry, entrypquota) in allentries :
498                if not changed.has_key(entry.Name) :
499                    changed[entry.Name] = {}
500                    if not options["groups"] :
501                        changed[entry.Name]["ingroups"] = []
502                       
503                if not entry.Exists :       
504                    # not found
505                    if options["add"] :
506                        # In case we want to add something, it is crucial
507                        # that we DON'T check with the system accounts files
508                        # like /etc/passwd because users may be defined
509                        # only remotely
510                        if self.isValidName(entry.Name) :
511                            entry = getattr(self.storage, "add%s" % suffix)(entry)
512                        else :   
513                            if options["groups"] :
514                                self.logger.log_message(_("Invalid group name %s") % entry.Name)
515                            else :   
516                                self.logger.log_message(_("Invalid user name %s") % entry.Name)
517                               
518                if not entrypquota.Exists :
519                    # not found
520                    if options["add"] :
521                        entrypquota = getattr(self.storage, "add%sPQuota" % suffix)(entry, printer)
522                       
523                if not entrypquota.Exists :     
524                    self.logger.log_message(_("Quota not found for object %s on printer %s.") % (entry.Name, printer.Name))
525                else :   
526                    if options["delete"] :
527                        entry.delete()
528                    else :
529                        if options["noquota"] or options["prototype"] or ((softlimit is not None) and (hardlimit is not None)) :
530                            entrypquota.setLimits(softlimit, hardlimit)
531                        if limitby :
532                            if changed[entry.Name].get("limitby") is None :
533                                entry.setLimitBy(limitby)
534                                changed[entry.Name]["limitby"] = limitby
535                       
536                        if not options["groups"] :
537                            if options["reset"] :
538                                entrypquota.reset()
539                               
540                            if balance :
541                                if changed[entry.Name].get("balance") is None :
542                                    if balance.startswith("+") or balance.startswith("-") :
543                                        newbalance = float(entry.AccountBalance or 0.0) + balancevalue
544                                        newlifetimepaid = float(entry.LifeTimePaid or 0.0) + balancevalue
545                                        entry.setAccountBalance(newbalance, newlifetimepaid)
546                                    else :
547                                        diff = balancevalue - float(entry.AccountBalance or 0.0)
548                                        newlifetimepaid = float(entry.LifeTimePaid or 0.0) + diff
549                                        entry.setAccountBalance(balancevalue, newlifetimepaid)
550                                    changed[entry.Name]["balance"] = balance
551                                   
552                            for groupname in groupnames :       
553                                # not executed if option --ingroups is not used
554                                if groupname not in changed[entry.Name]["ingroups"] :
555                                    group = self.storage.getGroup(groupname)
556                                    if group.Exists :
557                                        self.storage.addUserToGroup(entry, group)
558                                        changed[entry.Name]["ingroups"].append(groupname)
559                                    else :
560                                        self.logger.log_message(_("Group %s not found in the PyKota Storage.") % groupname)
561                                       
562                        # This line disabled to prevent sending of unwanted email                 
563                        # messages if quota is reached at creation/modification time.
564                        # The check will be done at print time anyway.
565                        # getattr(self, "warn%sPQuota" % suffix)(entrypquota)   
566                     
567if __name__ == "__main__" : 
568    retcode = 0
569    try :
570        defaults = { \
571                     "printer" : "*", \
572                   }
573        short_options = "vhdc:l:b:i:naugrp:P:S:H:"
574        long_options = ["help", "version", "charge=", "delete", "limitby=", "balance=", "ingroups=", "noquota", "add", "users", "groups", "reset", "prototype=", "printer=", "softlimit=", "hardlimit="]
575       
576        # Initializes the command line tool
577        editor = EdPyKota(doc=__doc__)
578       
579        # parse and checks the command line
580        (options, args) = editor.parseCommandline(sys.argv[1:], short_options, long_options)
581       
582        # sets long options
583        options["help"] = options["h"] or options["help"]
584        options["version"] = options["v"] or options["version"]
585        options["add"] = options["a"] or options["add"]
586        options["users"] = options["u"] or options["users"]
587        options["groups"] = options["g"] or options["groups"]
588        options["prototype"] = options["p"] or options["prototype"]
589        options["printer"] = options["P"] or options["printer"] or defaults["printer"]
590        options["softlimit"] = options["S"] or options["softlimit"]
591        options["hardlimit"] = options["H"] or options["hardlimit"] 
592        options["reset"] = options["r"] or options["reset"] 
593        options["noquota"] = options["n"] or options["noquota"]
594        options["limitby"] = options["l"] or options["limitby"]
595        options["balance"] = options["b"] or options["balance"] 
596        options["delete"] = options["d"] or options["delete"] 
597        options["ingroups"] = options["i"] or options["ingroups"]
598        options["charge"] = options["c"] or options["charge"]
599       
600        if options["help"] :
601            editor.display_usage_and_quit()
602        elif options["version"] :
603            editor.display_version_and_quit()
604        elif options["users"] and options["groups"] :   
605            raise PyKotaToolError, _("incompatible options, see help.")
606        elif (options["add"] or options["prototype"]) and options["delete"] :   
607            raise PyKotaToolError, _("incompatible options, see help.")
608        elif (options["softlimit"] or options["hardlimit"]) and options["prototype"] :   
609            raise PyKotaToolError, _("incompatible options, see help.")
610        elif options["noquota"] and (options["prototype"] or options["hardlimit"] or options["softlimit"]) :
611            raise PyKotaToolError, _("incompatible options, see help.")
612        elif options["groups"] and (options["balance"] or options["ingroups"]) :
613            raise PyKotaToolError, _("incompatible options, see help.")
614        else :
615            retcode = editor.main(args, options)
616    except (PyKotaToolError, PyKotaConfigError, PyKotaStorageError), msg :           
617        sys.stderr.write("%s\n" % msg)
618        sys.stderr.flush()
619        retcode = -1
620
621    try :
622        editor.storage.close()
623    except (TypeError, NameError, AttributeError) :   
624        pass
625       
626    sys.exit(retcode)   
Note: See TracBrowser for help on using the browser.