comparison roundup/admin.py @ 6959:3211745e8d7c

flake8 fixes.
author John Rouillard <rouilj@ieee.org>
date Mon, 12 Sep 2022 21:52:52 -0400
parents f924af12ef50
children d3d20e145cea
comparison
equal deleted inserted replaced
6958:e54a2db40a9e 6959:3211745e8d7c
21 """ 21 """
22 from __future__ import print_function 22 from __future__ import print_function
23 23
24 __docformat__ = 'restructuredtext' 24 __docformat__ = 'restructuredtext'
25 25
26 import csv, getopt, getpass, operator, os, re, shutil, sys 26 import csv
27 import getopt
28 import getpass
29 import operator
30 import os
31 import re
32 import shutil
33 import sys
27 34
28 from roundup import date, hyperdb, init, password, token 35 from roundup import date, hyperdb, init, password, token
29 from roundup import __version__ as roundup_version 36 from roundup import __version__ as roundup_version
30 import roundup.instance 37 import roundup.instance
31 from roundup.configuration import (CoreConfig, NoConfigError, 38 from roundup.configuration import (CoreConfig, NoConfigError,
155 h = _(command.__doc__).split('\n')[0] 162 h = _(command.__doc__).split('\n')[0]
156 commands.append(' '+h[7:]) 163 commands.append(' '+h[7:])
157 commands.sort() 164 commands.sort()
158 commands.append(_( 165 commands.append(_(
159 """Commands may be abbreviated as long as the abbreviation 166 """Commands may be abbreviated as long as the abbreviation
160 matches only one command, e.g. l == li == lis == list.""")) 167 matches only one command, e.g. l == li == lis == list.""")) # noqa: E122
161 sys.stdout.write('\n'.join(commands) + '\n\n') 168 sys.stdout.write('\n'.join(commands) + '\n\n')
162 169
163 def help_commands_html(self, indent_re=re.compile(r'^(\s+)\S+')): 170 indent_re = re.compile(r'^(\s+)\S+')
171
172 def help_commands_html(self, indent_re=indent_re):
164 """ Produce an HTML command list. 173 """ Produce an HTML command list.
165 """ 174 """
166 commands = sorted(iter(self.commands.values()), 175 commands = sorted(iter(self.commands.values()),
167 key=operator.attrgetter('__name__')) 176 key=operator.attrgetter('__name__'))
168 for command in commands: 177 for command in commands:
172 print(""" 181 print("""
173 <tr><td valign=top><strong>%(name)s</strong></td> 182 <tr><td valign=top><strong>%(name)s</strong></td>
174 <td><tt>%(usage)s</tt><p> 183 <td><tt>%(usage)s</tt><p>
175 <pre>""" % locals()) 184 <pre>""" % locals())
176 indent = indent_re.match(h[3]) 185 indent = indent_re.match(h[3])
177 if indent: indent = len(indent.group(1)) 186 if indent: indent = len(indent.group(1)) # noqa: E701
178 for line in h[3:]: 187 for line in h[3:]:
179 if indent: 188 if indent:
180 print(line[indent:]) 189 print(line[indent:])
181 else: 190 else:
182 print(line) 191 print(line)
247 """)) 256 """))
248 for name, command in list(self.commands.items()): 257 for name, command in list(self.commands.items()):
249 print(_('%s:') % name) 258 print(_('%s:') % name)
250 print(' ', _(command.__doc__)) 259 print(' ', _(command.__doc__))
251 260
252 def do_help(self, args, nl_re=re.compile('[\r\n]'), 261 nl_re = re.compile('[\r\n]')
253 indent_re=re.compile(r'^(\s+)\S+')): 262 # indent_re defined above
263
264 def do_help(self, args, nl_re=nl_re, indent_re=indent_re):
254 ''"""Usage: help topic 265 ''"""Usage: help topic
255 Give help about topic. 266 Give help about topic.
256 267
257 commands -- list commands 268 commands -- list commands
258 <command> -- help specific to a command 269 <command> -- help specific to a command
279 # display the help for each match, removing the docstring indent 290 # display the help for each match, removing the docstring indent
280 for _name, help in cmd_docs: 291 for _name, help in cmd_docs:
281 lines = nl_re.split(_(help.__doc__)) 292 lines = nl_re.split(_(help.__doc__))
282 print(lines[0]) 293 print(lines[0])
283 indent = indent_re.match(lines[1]) 294 indent = indent_re.match(lines[1])
284 if indent: indent = len(indent.group(1)) 295 if indent: indent = len(indent.group(1)) # noqa: E701
285 for line in lines[1:]: 296 for line in lines[1:]:
286 if indent: 297 if indent:
287 print(line[indent:]) 298 print(line[indent:])
288 else: 299 else:
289 print(line) 300 print(line)
318 # (2 dirs up) 329 # (2 dirs up)
319 # 330 #
320 # we're interested in where the directory containing "share" is 331 # we're interested in where the directory containing "share" is
321 debug = False 332 debug = False
322 templates = {} 333 templates = {}
323 if debug: print(__file__) 334 if debug: print(__file__) # noqa: E701
324 for N in 2, 4, 5, 6: 335 for N in 2, 4, 5, 6:
325 path = __file__ 336 path = __file__
326 # move up N elements in the path 337 # move up N elements in the path
327 for _i in range(N): 338 for _i in range(N):
328 path = os.path.dirname(path) 339 path = os.path.dirname(path)
329 tdir = os.path.join(path, 'share', 'roundup', 'templates') 340 tdir = os.path.join(path, 'share', 'roundup', 'templates')
330 if debug or trace_search: print(tdir) 341 if debug or trace_search: print(tdir) # noqa: E701
331 if os.path.isdir(tdir): 342 if os.path.isdir(tdir):
332 templates = init.listTemplates(tdir) 343 templates = init.listTemplates(tdir)
333 if debug: print(" Found templates breaking loop") 344 if debug: print(" Found templates breaking loop") # noqa: E701
334 break 345 break
335 346
336 # search for data files parallel to the roundup 347 # search for data files parallel to the roundup
337 # install dir. E.G. a wheel install 348 # install dir. E.G. a wheel install
338 # use roundup.__path__ and go up a level then use sys.prefix 349 # use roundup.__path__ and go up a level then use sys.prefix
347 for _N in 1, 2: 358 for _N in 1, 2:
348 path = os.path.dirname(path) 359 path = os.path.dirname(path)
349 # path is /usr/local/lib/python3.10/site-packages 360 # path is /usr/local/lib/python3.10/site-packages
350 tdir = os.path.join(path, sys.prefix[1:], 'share', 361 tdir = os.path.join(path, sys.prefix[1:], 'share',
351 'roundup', 'templates') 362 'roundup', 'templates')
352 if debug or trace_search: print(tdir) 363 if debug or trace_search: print(tdir) # noqa: E701
353 if os.path.isdir(tdir): 364 if os.path.isdir(tdir):
354 templates.update(init.listTemplates(tdir)) 365 templates.update(init.listTemplates(tdir))
355 366
356 try: 367 try:
357 # sigh pip 3.10 in virtual env finds another place to bury them. 368 # sigh pip 3.10 in virtual env finds another place to bury them.
358 # why local and sys.base_prefix are in path I do not know. 369 # why local and sys.base_prefix are in path I do not know.
359 # path is /usr/local/lib/python3.10/site-packages 370 # path is /usr/local/lib/python3.10/site-packages
360 tdir = os.path.join(path, sys.base_prefix[1:], 'local', 'share', 371 tdir = os.path.join(path, sys.base_prefix[1:], 'local', 'share',
361 'roundup', 'templates') 372 'roundup', 'templates')
362 if debug or trace_search: print(tdir) 373 if debug or trace_search: print(tdir) # noqa: E701
363 if os.path.isdir(tdir): 374 if os.path.isdir(tdir):
364 templates.update(init.listTemplates(tdir)) 375 templates.update(init.listTemplates(tdir))
365 # path is /usr/local/lib/python3.10/site-packages 376 # path is /usr/local/lib/python3.10/site-packages
366
367 377
368 tdir = os.path.join(path, sys.base_prefix[1:], 'share', 378 tdir = os.path.join(path, sys.base_prefix[1:], 'share',
369 'roundup', 'templates') 379 'roundup', 'templates')
370 if debug or trace_search: print(tdir) 380 if debug or trace_search: print(tdir) # noqa: E701
371 if os.path.isdir(tdir): 381 if os.path.isdir(tdir):
372 templates.update(init.listTemplates(tdir)) 382 templates.update(init.listTemplates(tdir))
373 except AttributeError: 383 except AttributeError:
374 pass # sys.base_prefix doesn't work under python2 384 pass # sys.base_prefix doesn't work under python2
375 385
376 # Try subdirs of the current dir 386 # Try subdirs of the current dir
377 templates.update(init.listTemplates(os.getcwd())) 387 templates.update(init.listTemplates(os.getcwd()))
378 if debug or trace_search: print(os.getcwd() + '/*') 388 if debug or trace_search: print(os.getcwd() + '/*') # noqa: E701
379 389
380 # Finally, try the current directory as a template 390 # Finally, try the current directory as a template
381 template = init.loadTemplateInfo(os.getcwd()) 391 template = init.loadTemplateInfo(os.getcwd())
382 if debug or trace_search: print(os.getcwd()) 392 if debug or trace_search: print(os.getcwd()) # noqa: E701
383 if template: 393 if template:
384 if debug: print(" Found template %s"%template['name']) 394 if debug: print(" Found template %s" % # noqa: E701
395 template['name'])
385 templates[template['name']] = template 396 templates[template['name']] = template
386 397
387 return templates 398 return templates
388 399
389 def help_initopts(self): 400 def help_initopts(self):
432 os.path.join(tracker_home, 'config.py')])): 443 os.path.join(tracker_home, 'config.py')])):
433 if not self.force: 444 if not self.force:
434 ok = my_input(_( 445 ok = my_input(_(
435 """WARNING: There appears to be a tracker in "%(tracker_home)s"! 446 """WARNING: There appears to be a tracker in "%(tracker_home)s"!
436 If you re-install it, you will lose all the data! 447 If you re-install it, you will lose all the data!
437 Erase it? Y/N: """) % locals()) 448 Erase it? Y/N: """) % locals()) # noqa: E122
438 if ok.strip().lower() != 'y': 449 if ok.strip().lower() != 'y':
439 return 0 450 return 0
440 451
441 # clear it out so the install isn't confused 452 # clear it out so the install isn't confused
442 shutil.rmtree(tracker_home) 453 shutil.rmtree(tracker_home)
521 ... see the documentation on customizing for more information. 532 ... see the documentation on customizing for more information.
522 533
523 You MUST run the "roundup-admin initialise" command once you've performed 534 You MUST run the "roundup-admin initialise" command once you've performed
524 the above steps. 535 the above steps.
525 --------------------------------------------------------------------------- 536 ---------------------------------------------------------------------------
526 """) % { 537 """) % {'database_config_file': os.path.join(tracker_home, 'schema.py'),
527 'database_config_file': os.path.join(tracker_home, 'schema.py'), 538 'database_init_file': os.path.join(tracker_home, 'initial_data.py')}) \
528 'database_init_file': os.path.join(tracker_home, 'initial_data.py'), 539 # noqa: E122
529 })
530 return 0 540 return 0
531 541
532 def _get_choice(self, list_name, prompt, options, argument, default=None): 542 def _get_choice(self, list_name, prompt, options, argument, default=None):
533 if default is None: 543 if default is None:
534 default = options[0] # just pick the first one 544 default = options[0] # just pick the first one
596 if tracker.exists(): 606 if tracker.exists():
597 if not self.force: 607 if not self.force:
598 ok = my_input(_( 608 ok = my_input(_(
599 """WARNING: The database is already initialised! 609 """WARNING: The database is already initialised!
600 If you re-initialise it, you will lose all the data! 610 If you re-initialise it, you will lose all the data!
601 Erase it? Y/N: """)) 611 Erase it? Y/N: """)) # noqa: E122
602 if ok.strip().lower() != 'y': 612 if ok.strip().lower() != 'y':
603 return 0 613 return 0
604 614
605 # nuke it 615 # nuke it
606 tracker.nuke() 616 tracker.nuke()
653 # print 663 # print
654 properties = cl.getprops() 664 properties = cl.getprops()
655 property = properties[propname] 665 property = properties[propname]
656 if not (isinstance(property, hyperdb.Multilink) or 666 if not (isinstance(property, hyperdb.Multilink) or
657 isinstance(property, hyperdb.Link)): 667 isinstance(property, hyperdb.Link)):
658 raise UsageError(_('property %s is not of type' 668 raise UsageError(_(
669 'property %s is not of type'
659 ' Multilink or Link so -d flag does not ' 670 ' Multilink or Link so -d flag does not '
660 'apply.') % propname) 671 'apply.') % propname)
661 propclassname = self.db.getclass(property.classname).classname 672 propclassname = self.db.getclass(property.classname).classname
662 id = cl.get(nodeid, propname) 673 id = cl.get(nodeid, propname)
663 for i in id: 674 for i in id:
670 if self.print_designator: 681 if self.print_designator:
671 properties = cl.getprops() 682 properties = cl.getprops()
672 property = properties[propname] 683 property = properties[propname]
673 if not (isinstance(property, hyperdb.Multilink) or 684 if not (isinstance(property, hyperdb.Multilink) or
674 isinstance(property, hyperdb.Link)): 685 isinstance(property, hyperdb.Link)):
675 raise UsageError(_('property %s is not of type' 686 raise UsageError(_(
687 'property %s is not of type'
676 ' Multilink or Link so -d flag does not ' 688 ' Multilink or Link so -d flag does not '
677 'apply.') % propname) 689 'apply.') % propname)
678 propclassname = self.db.getclass(property.classname).classname 690 propclassname = self.db.getclass(property.classname).classname
679 id = cl.get(nodeid, propname) 691 id = cl.get(nodeid, propname)
680 for i in id: 692 for i in id:
801 lastprop = pn # get current component 813 lastprop = pn # get current component
802 # get classname for this link 814 # get classname for this link
803 try: 815 try:
804 curclassname = curclass.getprops()[pn].classname 816 curclassname = curclass.getprops()[pn].classname
805 except KeyError: 817 except KeyError:
806 raise UsageError(_("Class %(curclassname)s has " 818 raise UsageError(_(
807 "no property %(pn)s in %(propname)s." % locals())) 819 "Class %(curclassname)s has "
820 "no property %(pn)s in %(propname)s." %
821 locals()))
808 # get class object 822 # get class object
809 curclass = self.get_class(curclassname) 823 curclass = self.get_class(curclassname)
810 except AttributeError: 824 except AttributeError:
811 # curclass.getprops()[pn].classname raises this 825 # curclass.getprops()[pn].classname raises this
812 # when we are at a non link/multilink property 826 # when we are at a non link/multilink property
980 props = {} 994 props = {}
981 properties = cl.getprops(protected=0) 995 properties = cl.getprops(protected=0)
982 if len(args) == 1: 996 if len(args) == 1:
983 # ask for the properties 997 # ask for the properties
984 for key in properties: 998 for key in properties:
985 if key == 'id': continue 999 if key == 'id': continue # noqa: E701
986 value = properties[key] 1000 value = properties[key]
987 name = value.__class__.__name__ 1001 name = value.__class__.__name__
988 if isinstance(value, hyperdb.Password): 1002 if isinstance(value, hyperdb.Password):
989 again = None 1003 again = None
990 while value != again: 1004 while value != again:
992 % 1006 %
993 {'propname': key.capitalize()}) 1007 {'propname': key.capitalize()})
994 again = getpass.getpass(_(' %(propname)s (Again): ') 1008 again = getpass.getpass(_(' %(propname)s (Again): ')
995 % 1009 %
996 {'propname': key.capitalize()}) 1010 {'propname': key.capitalize()})
997 if value != again: print(_('Sorry, try again...')) 1011 if value != again:
1012 print(_('Sorry, try again...'))
998 if value: 1013 if value:
999 props[key] = value 1014 props[key] = value
1000 else: 1015 else:
1001 value = my_input(_('%(propname)s (%(proptype)s): ') % { 1016 value = my_input(_('%(propname)s (%(proptype)s): ') % {
1002 'propname': key.capitalize(), 'proptype': name}) 1017 'propname': key.capitalize(), 'proptype': name})
1007 1022
1008 # convert types 1023 # convert types
1009 for propname in props: 1024 for propname in props:
1010 try: 1025 try:
1011 props[propname] = hyperdb.rawToHyperdb(self.db, cl, None, 1026 props[propname] = hyperdb.rawToHyperdb(self.db, cl, None,
1012 propname, props[propname]) 1027 propname,
1028 props[propname])
1013 except hyperdb.HyperdbValueError as message: 1029 except hyperdb.HyperdbValueError as message:
1014 raise UsageError(message) 1030 raise UsageError(message)
1015 1031
1016 # check for the key property 1032 # check for the key property
1017 propname = cl.getkey() 1033 propname = cl.getkey()
1095 1111
1096 templates = self.listTemplates(trace_search=trace_search) 1112 templates = self.listTemplates(trace_search=trace_search)
1097 1113
1098 for name in sorted(list(templates.keys())): 1114 for name in sorted(list(templates.keys())):
1099 templates[name]['description'] = textwrap.fill( 1115 templates[name]['description'] = textwrap.fill(
1100 "\n".join([ line.lstrip() for line in 1116 "\n".join([line.lstrip() for line in
1101 templates[name]['description'].split("\n")]), 1117 templates[name]['description'].split("\n")]),
1102 70, 1118 70,
1103 subsequent_indent=" " 1119 subsequent_indent=" "
1104 ) 1120 )
1105 print(""" 1121 print("""
1106 Name: %(name)s 1122 Name: %(name)s
1107 Path: %(path)s 1123 Path: %(path)s
1108 Desc: %(description)s 1124 Desc: %(description)s
1109 """%templates[name]) 1125 """ % templates[name])
1110 1126
1111 def do_table(self, args): 1127 def do_table(self, args):
1112 ''"""Usage: table classname [property[,property]*] 1128 ''"""Usage: table classname [property[,property]*]
1113 List the instances of a class in tabular form. 1129 List the instances of a class in tabular form.
1114 1130
1397 # on imports to rdbms. 1413 # on imports to rdbms.
1398 all_nodes = cl.getnodeids() 1414 all_nodes = cl.getnodeids()
1399 1415
1400 classkey = cl.getkey() 1416 classkey = cl.getkey()
1401 if classkey: # False sorts before True, so negate is_retired 1417 if classkey: # False sorts before True, so negate is_retired
1402 keysort = lambda i: (cl.get(i, classkey), 1418 keysort = lambda i: (cl.get(i, classkey), # noqa: E731
1403 not cl.is_retired(i)) 1419 not cl.is_retired(i))
1404 all_nodes.sort(key=keysort) 1420 all_nodes.sort(key=keysort)
1405 # if there is no classkey no need to sort 1421 # if there is no classkey no need to sort
1406 1422
1407 for nodeid in all_nodes: 1423 for nodeid in all_nodes:
1578 pack_before = date.Date(value) 1594 pack_before = date.Date(value)
1579 self.db.pack(pack_before) 1595 self.db.pack(pack_before)
1580 self.db_uncommitted = True 1596 self.db_uncommitted = True
1581 return 0 1597 return 0
1582 1598
1583 def do_reindex(self, args, desre=re.compile('([A-Za-z]+)([0-9]+)')): 1599 designator_re = re.compile('([A-Za-z]+)([0-9]+)')
1600
1601 def do_reindex(self, args, desre=designator_re):
1584 ''"""Usage: reindex [classname|designator]* 1602 ''"""Usage: reindex [classname|designator]*
1585 Re-generate a tracker's search indexes. 1603 Re-generate a tracker's search indexes.
1586 1604
1587 This will re-generate the search indexes for a tracker. 1605 This will re-generate the search indexes for a tracker.
1588 This will typically happen automatically. 1606 This will typically happen automatically.
1635 sys.stdout.write(_('Role "%(name)s":\n') % role.__dict__) 1653 sys.stdout.write(_('Role "%(name)s":\n') % role.__dict__)
1636 for permission in role.permissions: 1654 for permission in role.permissions:
1637 d = permission.__dict__ 1655 d = permission.__dict__
1638 if permission.klass: 1656 if permission.klass:
1639 if permission.properties: 1657 if permission.properties:
1640 sys.stdout.write(_(' %(description)s (%(name)s for "%(klass)s"' + 1658 sys.stdout.write(_(
1641 ': %(properties)s only)\n') % d) 1659 ' %(description)s (%(name)s for "%(klass)s"' +
1660 ': %(properties)s only)\n') % d)
1642 # verify that properties exist; report bad props 1661 # verify that properties exist; report bad props
1643 bad_props = [] 1662 bad_props = []
1644 cl = self.db.getclass(permission.klass) 1663 cl = self.db.getclass(permission.klass)
1645 class_props = cl.getprops(protected=True) 1664 class_props = cl.getprops(protected=True)
1646 for p in permission.properties: 1665 for p in permission.properties:
1647 if p in class_props: 1666 if p in class_props:
1648 continue 1667 continue
1649 else: 1668 else:
1650 bad_props.append(p) 1669 bad_props.append(p)
1651 if bad_props: 1670 if bad_props:
1652 sys.stdout.write(_('\n **Invalid properties for %(class)s: %(props)s\n\n') % {"class": permission.klass, "props": bad_props}) 1671 sys.stdout.write(_(
1672 '\n **Invalid properties for %(class)s: '
1673 '%(props)s\n\n') % {
1674 "class": permission.klass,
1675 "props": bad_props})
1653 return 1 1676 return 1
1654 else: 1677 else:
1655 sys.stdout.write(_(' %(description)s (%(name)s for ' 1678 sys.stdout.write(_(' %(description)s (%(name)s for '
1656 '"%(klass)s" only)\n') % d) 1679 '"%(klass)s" only)\n') % d)
1657 else: 1680 else:
1677 using anydbm). 1700 using anydbm).
1678 1701
1679 It's safe to run this even if it's not required, so just get 1702 It's safe to run this even if it's not required, so just get
1680 into the habit. 1703 into the habit.
1681 """ 1704 """
1682 if getattr(self.db, 'db_version_updated'): 1705 if self.db.db_version_updated:
1683 print(_('Tracker updated')) 1706 print(_('Tracker updated'))
1684 self.db_uncommitted = True 1707 self.db_uncommitted = True
1685 else: 1708 else:
1686 print(_('No migration action required')) 1709 print(_('No migration action required'))
1687 return 0 1710 return 0
1753 return 1 1776 return 1
1754 except NoConfigError as message: # noqa: F841 1777 except NoConfigError as message: # noqa: F841
1755 self.tracker_home = '' 1778 self.tracker_home = ''
1756 print(_("Error: Couldn't open tracker: %(message)s") % locals()) 1779 print(_("Error: Couldn't open tracker: %(message)s") % locals())
1757 return 1 1780 return 1
1758 except ParsingOptionError as message: # message used via locals 1781 # message used via locals
1782 except ParsingOptionError as message: # noqa: F841
1759 print("%(message)s" % locals()) 1783 print("%(message)s" % locals())
1760 return 1 1784 return 1
1761 1785
1762 # only open the database once! 1786 # only open the database once!
1763 if not self.db: 1787 if not self.db:
1764 self.db = tracker.open(self.name) 1788 self.db = tracker.open(self.name)
1765 # dont use tracker.config["TRACKER_LANGUAGE"] here as the 1789 # dont use tracker.config["TRACKER_LANGUAGE"] here as the
1766 # cli operator likely wants to have i18n as set in the 1790 # cli operator likely wants to have i18n as set in the
1767 # environment. 1791 # environment.
1768 # This is needed to fetch the locale's of the tracker's home dir. 1792 # This is needed to fetch the locale's of the tracker's home dir.
1769 self.db.i18n = get_translation (tracker_home = tracker.tracker_home) 1793 self.db.i18n = get_translation(tracker_home=tracker.tracker_home)
1770 1794
1771 self.db.tx_Source = 'cli' 1795 self.db.tx_Source = 'cli'
1772 1796
1773 # do the command 1797 # do the command
1774 ret = 0 1798 ret = 0
1799 try: 1823 try:
1800 command = my_input(_('roundup> ')) 1824 command = my_input(_('roundup> '))
1801 except EOFError: 1825 except EOFError:
1802 print(_('exit...')) 1826 print(_('exit...'))
1803 break 1827 break
1804 if not command: continue 1828 if not command: continue # noqa: E701
1805 try: 1829 try:
1806 args = token.token_split(command) 1830 args = token.token_split(command)
1807 except ValueError: 1831 except ValueError:
1808 continue # Ignore invalid quoted token 1832 continue # Ignore invalid quoted token
1809 if not args: continue 1833 if not args: continue # noqa: E701
1810 if args[0] in ('quit', 'exit'): break 1834 if args[0] in ('quit', 'exit'): break # noqa: E701
1811 self.run_command(args) 1835 self.run_command(args)
1812 1836
1813 # exit.. check for transactions 1837 # exit.. check for transactions
1814 if self.db and self.db_uncommitted: 1838 if self.db and self.db_uncommitted:
1815 commit = my_input(_('There are unsaved changes. Commit them (y/N)? ')) 1839 commit = my_input(_('There are unsaved changes. Commit them (y/N)? '))
1877 try: 1901 try:
1878 if not args: 1902 if not args:
1879 self.interactive() 1903 self.interactive()
1880 else: 1904 else:
1881 ret = self.run_command(args) 1905 ret = self.run_command(args)
1882 if self.db: self.db.commit() 1906 if self.db: self.db.commit() # noqa: E701
1883 return ret 1907 return ret
1884 finally: 1908 finally:
1885 if self.db: 1909 if self.db:
1886 self.db.close() 1910 self.db.close()
1887 1911

Roundup Issue Tracker: http://roundup-tracker.org/