root / pykota / trunk / bin / edpykota @ 1205

Revision 1180, 26.2 kB (checked in by jalet, 21 years ago)

More work on new backend. This commit may be unstable.

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