ProjectFlask/Project.py

changeset 17
f31df56510a1
parent 16
dd3f6bfb85f7
child 18
d76a0939be6a
equal deleted inserted replaced
16:dd3f6bfb85f7 17:f31df56510a1
6 """ 6 """
7 Module implementing the Flask project support. 7 Module implementing the Flask project support.
8 """ 8 """
9 9
10 import os 10 import os
11 import re
12 11
13 from PyQt5.QtCore import ( 12 from PyQt5.QtCore import (
14 pyqtSlot, QObject, QProcess, QProcessEnvironment, QTimer 13 pyqtSlot, QObject, QProcess, QProcessEnvironment, QTimer
15 ) 14 )
16 from PyQt5.QtWidgets import QMenu, QDialog 15 from PyQt5.QtWidgets import QMenu
17 16
18 from E5Gui import E5MessageBox 17 from E5Gui import E5MessageBox
19 from E5Gui.E5Action import E5Action 18 from E5Gui.E5Action import E5Action
20 from E5Gui.E5Application import e5App 19 from E5Gui.E5Application import e5App
21 20
23 22
24 import UI.PixmapCache 23 import UI.PixmapCache
25 import Utilities 24 import Utilities
26 25
27 from .FlaskCommandDialog import FlaskCommandDialog 26 from .FlaskCommandDialog import FlaskCommandDialog
28 from .PyBabelCommandDialog import PyBabelCommandDialog 27
29 28 from .FlaskBabelExtension.PyBabelProjectExtension import PyBabelProject
30 29
31 # TODO: move PyBabel related code to a separate package (FlaskBabelExtension) 30
32 # TODO: move database related code to a separate package (FlaskMigrateExtension) 31 # TODO: move database related code to a separate package (FlaskMigrateExtension)
33 class Project(QObject): 32 class Project(QObject):
34 """ 33 """
35 Class implementing the Flask project support. 34 Class implementing the Flask project support.
36 """ 35 """
70 "python": "", 69 "python": "",
71 "flask": "", 70 "flask": "",
72 "werkzeug": "", 71 "werkzeug": "",
73 } 72 }
74 73
75 self.__capabilities = { 74 self.__capabilities = {}
76 "pybabel": False, 75
77 "migrate": False, 76 self.__pybabelProject = PyBabelProject(self.__plugin, self, self.__ui)
78 }
79 77
80 def initActions(self): 78 def initActions(self):
81 """ 79 """
82 Public method to define the Flask actions. 80 Public method to define the Flask actions.
83 """ 81 """
199 'Shows a dialog to edit the configuration for pybabel')) 197 'Shows a dialog to edit the configuration for pybabel'))
200 self.pybabelConfigAct.setWhatsThis(self.tr( 198 self.pybabelConfigAct.setWhatsThis(self.tr(
201 """<b>Configure PyBabel</b>""" 199 """<b>Configure PyBabel</b>"""
202 """<p>Shows a dialog to edit the configuration for pybabel.</p>""" 200 """<p>Shows a dialog to edit the configuration for pybabel.</p>"""
203 )) 201 ))
204 self.pybabelConfigAct.triggered.connect(self.__configurePybabel) 202 self.pybabelConfigAct.triggered.connect(
203 self.__pybabelProject.configurePyBabel)
205 self.actions.append(self.pybabelConfigAct) 204 self.actions.append(self.pybabelConfigAct)
206 205
207 ################################## 206 ##################################
208 ## documentation action below ## 207 ## documentation action below ##
209 ################################## 208 ##################################
294 @return menu names 293 @return menu names
295 @rtype list of str 294 @rtype list of str
296 """ 295 """
297 return list(self.__menus.keys()) 296 return list(self.__menus.keys())
298 297
299 def registerOpenHook(self):
300 """
301 Public method to register the open hook to open a translations file
302 in a translations editor.
303 """
304 if self.__hooksInstalled:
305 editor = self.__plugin.getPreferences("TranslationsEditor")
306 if editor:
307 self.__translationsBrowser.addHookMethodAndMenuEntry(
308 "open", self.openPOEditor,
309 self.tr("Open with {0}").format(
310 os.path.basename(editor)))
311 else:
312 self.__translationsBrowser.removeHookMethod("open")
313
314 def projectOpenedHooks(self): 298 def projectOpenedHooks(self):
315 """ 299 """
316 Public method to add our hook methods. 300 Public method to add our hook methods.
317 """ 301 """
318 if self.__e5project.getProjectType() == "Flask": 302 if self.__e5project.getProjectType() == "Flask":
323 ## self.__formsBrowser.addHookMethodAndMenuEntry( 307 ## self.__formsBrowser.addHookMethodAndMenuEntry(
324 ## "newForm", self.newForm, self.tr("New template...")) 308 ## "newForm", self.newForm, self.tr("New template..."))
325 ## 309 ##
326 self.__determineCapabilities() 310 self.__determineCapabilities()
327 311
328 if self.__capabilities["pybabel"]: 312 self.__pybabelProject.projectOpenedHooks()
329 self.__e5project.projectLanguageAddedByCode.connect( 313 ## self.__hooksInstalled = True
330 self.__projectLanguageAdded)
331 self.__translationsBrowser = (
332 e5App().getObject("ProjectBrowser")
333 .getProjectBrowser("translations"))
334 self.__translationsBrowser.addHookMethodAndMenuEntry(
335 "extractMessages", self.extractMessages,
336 self.tr("Extract Messages"))
337 self.__translationsBrowser.addHookMethodAndMenuEntry(
338 "releaseAll", self.compileCatalogs,
339 self.tr("Compile All Catalogs"))
340 self.__translationsBrowser.addHookMethodAndMenuEntry(
341 "releaseSelected", self.compileSelectedCatalogs,
342 self.tr("Compile Selected Catalogs"))
343 self.__translationsBrowser.addHookMethodAndMenuEntry(
344 "generateAll", self.updateCatalogs,
345 self.tr("Update All Catalogs"))
346 self.__translationsBrowser.addHookMethodAndMenuEntry(
347 "generateAllWithObsolete", self.updateCatalogsObsolete,
348 self.tr("Update All Catalogs (with obsolete)"))
349 self.__translationsBrowser.addHookMethodAndMenuEntry(
350 "generateSelected", self.updateSelectedCatalogs,
351 self.tr("Update Selected Catalogs"))
352 self.__translationsBrowser.addHookMethodAndMenuEntry(
353 "generateSelectedWithObsolete",
354 self.updateSelectedCatalogsObsolete,
355 self.tr("Update Selected Catalogs (with obsolete)"))
356
357 self.__hooksInstalled = True
358
359 self.registerOpenHook()
360 314
361 def projectClosedHooks(self): 315 def projectClosedHooks(self):
362 """ 316 """
363 Public method to remove our hook methods. 317 Public method to remove our hook methods.
364 """ 318 """
319 self.__pybabelProject.projectClosedHooks()
320
365 if self.__hooksInstalled: 321 if self.__hooksInstalled:
366 ## self.__formsBrowser.removeHookMethod("newForm") 322 ## self.__formsBrowser.removeHookMethod("newForm")
367 ## self.__formsBrowser = None 323 ## self.__formsBrowser = None
368 ## 324 pass
369 self.__e5project.projectLanguageAddedByCode.disconnect(
370 self.__projectLanguageAdded)
371 self.__translationsBrowser.removeHookMethod(
372 "extractMessages")
373 self.__translationsBrowser.removeHookMethod(
374 "releaseAll")
375 self.__translationsBrowser.removeHookMethod(
376 "releaseSelected")
377 self.__translationsBrowser.removeHookMethod(
378 "generateAll")
379 self.__translationsBrowser.removeHookMethod(
380 "generateAllWithObsolete")
381 self.__translationsBrowser.removeHookMethod(
382 "generateSelected")
383 self.__translationsBrowser.removeHookMethod(
384 "generateSelectedWithObsolete")
385 self.__translationsBrowser.removeHookMethod(
386 "open")
387 self.__translationsBrowser = None
388 325
389 self.__hooksInstalled = False 326 self.__hooksInstalled = False
390 327
391 ################################################################## 328 ##################################################################
392 ## slots below implement general functionality 329 ## slots below implement general functionality
481 Public method to build the Flask command. 418 Public method to build the Flask command.
482 419
483 @return full flask command 420 @return full flask command
484 @rtype str 421 @rtype str
485 """ 422 """
486 return self.__getFullCommand("flask") 423 return self.getFullCommand("flask")
487 424
488 def getBabelCommand(self): 425 def getFullCommand(self, command):
489 """ 426 """
490 Public method to build the Babel command. 427 Public method to get the full command for a given command name.
491
492 @return full pybabel command
493 @rtype str
494 """
495 return self.__getFullCommand("pybabel")
496
497 def __getFullCommand(self, command):
498 """
499 Private method to get the full command for a given command name.
500 428
501 @param command command name 429 @param command command name
502 @type str 430 @type str
503 @return full command 431 @return full command
504 @rtype str 432 @rtype str
681 """ 609 """
682 Private method to determine capabilities provided by supported 610 Private method to determine capabilities provided by supported
683 extensions. 611 extensions.
684 """ 612 """
685 # 1. support for flask-babel (i.e. pybabel) 613 # 1. support for flask-babel (i.e. pybabel)
686 self.__capabilities["pybabel"] = self.flaskBabelAvailable() 614 self.__pybabelProject.determineCapability()
687 self.pybabelConfigAct.setEnabled(self.__capabilities["pybabel"]) 615 self.pybabelConfigAct.setEnabled(self.hasCapability("pybabel"))
688 616
689 # 2. support for flask-migrate 617 # 2. support for flask-migrate
690 # TODO: add support for flask-migrate 618 # TODO: add support for flask-migrate
619
620 def hasCapability(self, key):
621 """
622 Public method to check, if a capability is available.
623
624 @param key key of the capability to check
625 @type str
626 @return flag indicating the availability of the capability
627 @rtype bool
628 """
629 try:
630 return self.__capabilities[key]
631 except KeyError:
632 return False
633
634 def setCapability(self, key, available):
635 """
636 Public method to set the availability status of a capability.
637
638 @param key key of the capability to set
639 @type str
640 @param available flag indicating the availability of the capability
641 @type bool
642 """
643 self.__capabilities[key] = available
691 644
692 ################################################################## 645 ##################################################################
693 ## slot below implements project specific flask configuration 646 ## slot below implements project specific flask configuration
694 ################################################################## 647 ##################################################################
695 648
819 Private slot showing the result of the database creation. 772 Private slot showing the result of the database creation.
820 """ 773 """
821 dlg = FlaskCommandDialog(self) 774 dlg = FlaskCommandDialog(self)
822 if dlg.startCommand("init-db"): 775 if dlg.startCommand("init-db"):
823 dlg.exec() 776 dlg.exec()
824
825 ##################################################################
826 ## slots and methods below implement i18n and l10n support
827 ##################################################################
828
829 def flaskBabelAvailable(self):
830 """
831 Public method to check, if the 'flask-babel' package is available.
832
833 @return flag indicating the availability of 'flask-babel'
834 @rtype bool
835 """
836 venvName = self.__plugin.getPreferences("VirtualEnvironmentNamePy3")
837 interpreter = self.__virtualEnvManager.getVirtualenvInterpreter(
838 venvName)
839 if interpreter and Utilities.isinpath(interpreter):
840 detector = os.path.join(
841 os.path.dirname(__file__), "FlaskBabelDetector.py")
842 proc = QProcess()
843 proc.setProcessChannelMode(QProcess.MergedChannels)
844 proc.start(interpreter, [detector])
845 finished = proc.waitForFinished(30000)
846 if finished and proc.exitCode() == 0:
847 return True
848
849 return False
850
851 @pyqtSlot()
852 def __configurePybabel(self):
853 """
854 Private slot to show a dialog to edit the pybabel configuration.
855 """
856 from .PyBabelConfigDialog import PyBabelConfigDialog
857
858 config = self.getData("pybabel", "")
859 dlg = PyBabelConfigDialog(config)
860 if dlg.exec() == QDialog.Accepted:
861 config = dlg.getConfiguration()
862 self.setData("pybabel", "", config)
863
864 self.__e5project.setTranslationPattern(os.path.join(
865 config["translationsDirectory"], "%language%", "LC_MESSAGES",
866 "{0}.po".format(config["domain"])
867 ))
868 self.__e5project.setDirty(True)
869
870 cfgFileName = self.__e5project.getAbsoluteUniversalPath(
871 config["configFile"])
872 if not os.path.exists(cfgFileName):
873 self.__createBabelCfg(cfgFileName)
874
875 def __ensurePybabelConfigured(self):
876 """
877 Private method to ensure, that PyBabel has been configured.
878
879 @return flag indicating successful configuration
880 @rtype bool
881 """
882 config = self.getData("pybabel", "")
883 if not config:
884 self.__configurePybabel()
885 return True
886
887 configFileName = self.getData("pybabel", "configFile")
888 if configFileName:
889 cfgFileName = self.__e5project.getAbsoluteUniversalPath(
890 configFileName)
891 if os.path.exists(cfgFileName):
892 return True
893 else:
894 return self.__createBabelCfg(cfgFileName)
895
896 return False
897
898 def __createBabelCfg(self, configFile):
899 """
900 Private method to create a template pybabel configuration file.
901
902 @param configFile name of the configuration file to be created
903 @type str
904 @return flag indicating successful configuration file creation
905 @rtype bool
906 """
907 _, app = self.getApplication()
908 if app.endswith(".py"):
909 template = (
910 "[python: {0}]\n"
911 "[jinja2: templates/**.html]\n"
912 "extensions=jinja2.ext.autoescape,jinja2.ext.with_\n"
913 )
914 else:
915 template = (
916 "[python: {0}/**.py]\n"
917 "[jinja2: {0}/templates/**.html]\n"
918 "extensions=jinja2.ext.autoescape,jinja2.ext.with_\n"
919 )
920 try:
921 with open(configFile, "w") as f:
922 f.write(template.format(app))
923 self.__e5project.appendFile(configFile)
924 E5MessageBox.information(
925 None,
926 self.tr("Generate PyBabel Configuration File"),
927 self.tr("""The PyBabel configuration file was created."""
928 """ Please edit it to adjust the entries as"""
929 """ required.""")
930 )
931 return True
932 except EnvironmentError as err:
933 E5MessageBox.warning(
934 None,
935 self.tr("Generate PyBabel Configuration File"),
936 self.tr("""<p>The PyBabel Configuration File could not be"""
937 """ generated.</p><p>Reason: {0}</p>""")
938 .format(str(err))
939 )
940 return False
941
942 def __getLocale(self, filename):
943 """
944 Private method to extract the locale out of a file name.
945
946 @param filename name of the file used for extraction
947 @type str
948 @return extracted locale
949 @rtype str or None
950 """
951 if self.__e5project.getTranslationPattern():
952 filename = os.path.splitext(filename)[0] + ".po"
953
954 # On Windows, path typically contains backslashes. This leads
955 # to an invalid search pattern '...\(' because the opening bracket
956 # will be escaped.
957 pattern = self.__e5project.getTranslationPattern()
958 pattern = os.path.normpath(pattern)
959 pattern = pattern.replace("%language%", "(.*?)")
960 pattern = pattern.replace('\\', '\\\\')
961 match = re.search(pattern, filename)
962 if match is not None:
963 return match.group(1)
964
965 return None
966
967 def openPOEditor(self, poFile):
968 """
969 Public method to edit the given file in an external .po editor.
970
971 @param poFile name of the .po file
972 @type str
973 """
974 editor = self.__plugin.getPreferences("TranslationsEditor")
975 if poFile.endswith(".po") and editor:
976 wd, _ = self.getApplication()
977 started, pid = QProcess.startDetached(editor, [poFile], wd)
978 if not started:
979 E5MessageBox.critical(
980 None,
981 self.tr('Process Generation Error'),
982 self.tr('The translations editor process ({0}) could'
983 ' not be started.').format(
984 os.path.basename(editor)))
985
986 def extractMessages(self):
987 """
988 Public method to extract the messages catalog template file.
989 """
990 title = self.tr("Extract messages")
991 if self.__ensurePybabelConfigured():
992 workdir = self.getApplication()[0]
993 potFile = self.__e5project.getAbsoluteUniversalPath(
994 self.getData("pybabel", "catalogFile"))
995
996 try:
997 potFilePath = os.path.dirname(potFile)
998 os.makedirs(potFilePath)
999 except EnvironmentError:
1000 pass
1001
1002 args = [
1003 "-F",
1004 os.path.relpath(
1005 self.__e5project.getAbsoluteUniversalPath(
1006 self.getData("pybabel", "configFile")),
1007 workdir
1008 )
1009 ]
1010 if self.getData("pybabel", "markersList"):
1011 for marker in self.getData("pybabel", "markersList"):
1012 args += ["-k", marker]
1013 args += [
1014 "-o",
1015 os.path.relpath(potFile, workdir),
1016 "."
1017 ]
1018
1019 dlg = PyBabelCommandDialog(
1020 self, title,
1021 msgSuccess=self.tr("\nMessages extracted successfully.")
1022 )
1023 res = dlg.startCommand("extract", args, workdir)
1024 if res:
1025 dlg.exec()
1026 self.__e5project.appendFile(potFile)
1027
1028 def __projectLanguageAdded(self, code):
1029 """
1030 Private slot handling the addition of a new language.
1031
1032 @param code language code of the new language
1033 @type str
1034 """
1035 title = self.tr(
1036 "Initializing message catalog for '{0}'").format(code)
1037
1038 if self.__ensurePybabelConfigured():
1039 workdir = self.getApplication()[0]
1040 langFile = self.__e5project.getAbsoluteUniversalPath(
1041 self.__e5project.getTranslationPattern().replace(
1042 "%language%", code))
1043 potFile = self.__e5project.getAbsoluteUniversalPath(
1044 self.getData("pybabel", "catalogFile"))
1045
1046 args = [
1047 "--domain={0}".format(self.getData("pybabel", "domain")),
1048 "--input-file={0}".format(os.path.relpath(potFile, workdir)),
1049 "--output-file={0}".format(os.path.relpath(langFile, workdir)),
1050 "--locale={0}".format(code),
1051 ]
1052
1053 dlg = PyBabelCommandDialog(
1054 self, title,
1055 msgSuccess=self.tr(
1056 "\nMessage catalog initialized successfully.")
1057 )
1058 res = dlg.startCommand("init", args, workdir)
1059 if res:
1060 dlg.exec()
1061
1062 self.__e5project.appendFile(langFile)
1063
1064 def compileCatalogs(self, filenames):
1065 """
1066 Public method to compile the message catalogs.
1067
1068 @param filenames list of filenames (not used)
1069 @type list of str
1070 """
1071 title = self.tr("Compiling message catalogs")
1072
1073 if self.__ensurePybabelConfigured():
1074 workdir = self.getApplication()[0]
1075 translationsDirectory = self.__e5project.getAbsoluteUniversalPath(
1076 self.getData("pybabel", "translationsDirectory"))
1077
1078 args = [
1079 "--domain={0}".format(self.getData("pybabel", "domain")),
1080 "--directory={0}".format(
1081 os.path.relpath(translationsDirectory, workdir)),
1082 "--use-fuzzy",
1083 "--statistics",
1084 ]
1085
1086 dlg = PyBabelCommandDialog(
1087 self, title,
1088 msgSuccess=self.tr("\nMessage catalogs compiled successfully.")
1089 )
1090 res = dlg.startCommand("compile", args, workdir)
1091 if res:
1092 dlg.exec()
1093
1094 for entry in os.walk(translationsDirectory):
1095 for fileName in entry[2]:
1096 fullName = os.path.join(entry[0], fileName)
1097 if fullName.endswith('.mo'):
1098 self.__e5project.appendFile(fullName)
1099
1100 def compileSelectedCatalogs(self, filenames):
1101 """
1102 Public method to update the message catalogs.
1103
1104 @param filenames list of file names
1105 @type list of str
1106 """
1107 title = self.tr("Compiling message catalogs")
1108
1109 locales = {self.__getLocale(f) for f in filenames}
1110
1111 if len(locales) == 0:
1112 E5MessageBox.warning(
1113 self.__ui,
1114 title,
1115 self.tr('No locales detected. Aborting...'))
1116 return
1117
1118 if self.__ensurePybabelConfigured():
1119 workdir = self.getApplication()[0]
1120 translationsDirectory = self.__e5project.getAbsoluteUniversalPath(
1121 self.getData("pybabel", "translationsDirectory"))
1122
1123 argsList = []
1124 for loc in locales:
1125 argsList.append([
1126 "compile",
1127 "--domain={0}".format(self.getData("pybabel", "domain")),
1128 "--directory={0}".format(
1129 os.path.relpath(translationsDirectory, workdir)),
1130 "--use-fuzzy",
1131 "--statistics",
1132 "--locale={0}".format(loc),
1133 ])
1134
1135 dlg = PyBabelCommandDialog(
1136 self, title=title,
1137 msgSuccess=self.tr("\nMessage catalogs compiled successfully.")
1138 )
1139 res = dlg.startBatchCommand(argsList, workdir)
1140 if res:
1141 dlg.exec()
1142
1143 for entry in os.walk(translationsDirectory):
1144 for fileName in entry[2]:
1145 fullName = os.path.join(entry[0], fileName)
1146 if fullName.endswith('.mo'):
1147 self.__e5project.appendFile(fullName)
1148
1149 def updateCatalogs(self, filenames, withObsolete=False):
1150 """
1151 Public method to update the message catalogs.
1152
1153 @param filenames list of filenames (not used)
1154 @type list of str
1155 @param withObsolete flag indicating to keep obsolete translations
1156 @type bool
1157 """
1158 title = self.tr("Updating message catalogs")
1159
1160 if self.__ensurePybabelConfigured():
1161 workdir = self.getApplication()[0]
1162 translationsDirectory = self.__e5project.getAbsoluteUniversalPath(
1163 self.getData("pybabel", "translationsDirectory"))
1164 potFile = self.__e5project.getAbsoluteUniversalPath(
1165 self.getData("pybabel", "catalogFile"))
1166
1167 args = [
1168 "--domain={0}".format(self.getData("pybabel", "domain")),
1169 "--input-file={0}".format(os.path.relpath(potFile, workdir)),
1170 "--output-dir={0}".format(
1171 os.path.relpath(translationsDirectory, workdir)),
1172 ]
1173 if not withObsolete:
1174 args.append("--ignore-obsolete")
1175
1176 dlg = PyBabelCommandDialog(
1177 self, title,
1178 msgSuccess=self.tr("\nMessage catalogs updated successfully.")
1179 )
1180 res = dlg.startCommand("update", args, workdir)
1181 if res:
1182 dlg.exec()
1183
1184 def updateCatalogsObsolete(self, filenames):
1185 """
1186 Public method to update the message catalogs keeping obsolete
1187 translations.
1188
1189 @param filenames list of filenames (not used)
1190 @type list of str
1191 """
1192 self.updateCatalogs(filenames, withObsolete=True)
1193
1194 def updateSelectedCatalogs(self, filenames, withObsolete=False):
1195 """
1196 Public method to update the selected message catalogs.
1197
1198 @param filenames list of filenames
1199 @type list of str
1200 @param withObsolete flag indicating to keep obsolete translations
1201 @type bool
1202 """
1203 title = self.tr("Updating message catalogs")
1204
1205 locales = {self.__getLocale(f) for f in filenames}
1206
1207 if len(locales) == 0:
1208 E5MessageBox.warning(
1209 self.__ui,
1210 title,
1211 self.tr('No locales detected. Aborting...'))
1212 return
1213
1214 if self.__ensurePybabelConfigured():
1215 workdir = self.getApplication()[0]
1216 translationsDirectory = self.__e5project.getAbsoluteUniversalPath(
1217 self.getData("pybabel", "translationsDirectory"))
1218 potFile = self.__e5project.getAbsoluteUniversalPath(
1219 self.getData("pybabel", "catalogFile"))
1220 argsList = []
1221 for loc in locales:
1222 args = [
1223 "update",
1224 "--domain={0}".format(self.getData("pybabel", "domain")),
1225 "--input-file={0}".format(
1226 os.path.relpath(potFile, workdir)),
1227 "--output-dir={0}".format(
1228 os.path.relpath(translationsDirectory, workdir)),
1229 "--locale={0}".format(loc),
1230 ]
1231 if not withObsolete:
1232 args.append("--ignore-obsolete")
1233 argsList.append(args)
1234
1235 dlg = PyBabelCommandDialog(
1236 self, title=title,
1237 msgSuccess=self.tr("\nMessage catalogs updated successfully.")
1238 )
1239 res = dlg.startBatchCommand(argsList, workdir)
1240 if res:
1241 dlg.exec()
1242
1243 def updateSelectedCatalogsObsolete(self, filenames):
1244 """
1245 Public method to update the message catalogs keeping obsolete
1246 translations.
1247
1248 @param filenames list of filenames (not used)
1249 @type list of str
1250 """
1251 self.updateSelectedCatalogs(filenames, withObsolete=True)

eric ide

mercurial