PluginMetricsRadon.py

changeset 2
1ad320a50a01
parent 0
765bb3e711d6
child 3
7150ed890fd5
equal deleted inserted replaced
1:b6cced815847 2:1ad320a50a01
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2015 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing the radon code metrics plug-in.
8 """
9
10 from __future__ import unicode_literals
11
12 import os
13
14 from PyQt5.QtCore import pyqtSignal, QObject, QTranslator
15 from PyQt5.QtWidgets import QAction
16
17 from E5Gui.E5Application import e5App
18 from E5Gui.E5Action import E5Action
19
20 from Project.ProjectBrowserModel import ProjectBrowserFileItem
21
22 import Preferences
23 from Utilities import determinePythonVersion
24
25 # Start-Of-Header
26 name = "Radon Metrics Plugin"
27 author = "Detlev Offenbach <detlev@die-offenbachs.de>"
28 autoactivate = True
29 deactivateable = True
30 version = "0.1.0"
31 className = "RadonMetricsPlugin"
32 packageName = "RadonMetrics"
33 shortDescription = "Code metrics plugin using radon package"
34 longDescription = (
35 """This plug-in implements dialogs to show various code metrics. These"""
36 """ are determined using the radon code metrics package."""
37 )
38 needsRestart = False
39 pyqtApi = 2
40 python2Compatible = True
41 # End-Of-Header
42
43 error = ""
44
45
46 class RadonMetricsPlugin(QObject):
47 """
48 Class implementing the radon code metrics plug-in.
49
50 @signal metricsDone(str, list) emitted when the code metrics were
51 determined for a file
52 @signal metricsError(str, str) emitted in case of an error
53 @signal batchFinished() emitted when a code metrics batch is done
54 """
55 metricsDone = pyqtSignal(str, list) # TODO: adjust this
56 metricsError = pyqtSignal(str, str)
57 batchFinished = pyqtSignal()
58
59 def __init__(self, ui):
60 """
61 Constructor
62
63 @param ui reference to the user interface object
64 @type UI.UserInterface
65 """
66 super(RadonMetricsPlugin, self).__init__(ui)
67 self.__ui = ui
68 self.__initialize()
69
70 self.backgroundService = e5App().getObject("BackgroundService")
71
72 path = os.path.join(
73 os.path.dirname(__file__), packageName, 'Tabnanny')
74 self.backgroundService.serviceConnect(
75 'radon', 'Python2', path, 'CodeMetricsCalculator',
76 lambda *args: self.metricsDone.emit(*args),
77 onErrorCallback=self.serviceErrorPy2,
78 onBatchDone=self.batchJobDone)
79 self.backgroundService.serviceConnect(
80 'radon', 'Python3', path, 'CodeMetricsCalculator',
81 lambda *args: self.metricsDone.emit(*args),
82 onErrorCallback=self.serviceErrorPy3,
83 onBatchDone=self.batchJobDone)
84
85 self.queuedBatches = []
86 self.batchesFinished = True
87
88 self.__translator = None
89 self.__loadTranslator()
90
91 def __serviceError(self, fn, msg):
92 """
93 Private slot handling service errors.
94
95 @param fn file name
96 @type str
97 @param msg message text
98 @type str
99 """
100 self.metricsError.emit(fn, msg)
101
102 def serviceErrorPy2(self, fx, lang, fn, msg):
103 """
104 Public slot handling service errors for Python 2.
105
106 @param fx service name
107 @type str
108 @param lang language
109 @type str
110 @param fn file name
111 @type str
112 @param msg message text
113 @type str
114 """
115 if fx in ['radon', 'batch_radon'] and lang == 'Python2':
116 if fx == 'radon':
117 self.__serviceError(fn, msg)
118 else:
119 self.__serviceError(self.tr("Python 2 batch job"), msg)
120 self.batchJobDone(fx, lang)
121
122 def serviceErrorPy3(self, fx, lang, fn, msg):
123 """
124 Public slot handling service errors for Python 3.
125
126 @param fx service name
127 @type str
128 @param lang language
129 @type str
130 @param fn file name
131 @type str
132 @param msg message text
133 @type str
134 """
135 if fx in ['radon', 'batch_radon'] and lang == 'Python3':
136 if fx == 'radon':
137 self.__serviceError(fn, msg)
138 else:
139 self.__serviceError(self.tr("Python 3 batch job"), msg)
140 self.batchJobDone(fx, lang)
141
142 def batchJobDone(self, fx, lang):
143 """
144 Public slot handling the completion of a batch job.
145
146 @param fx service name
147 @type str
148 @param lang language
149 @type str
150 """
151 if fx in ['radon', 'batch_radon']:
152 if lang in self.queuedBatches:
153 self.queuedBatches.remove(lang)
154 # prevent sending the signal multiple times
155 if len(self.queuedBatches) == 0 and not self.batchesFinished:
156 self.batchFinished.emit()
157 self.batchesFinished = True
158
159 def __initialize(self):
160 """
161 Private slot to (re)initialize the plugin.
162 """
163 self.__projectRawMetricsAct = None
164 self.__projectRawMetricsDialog = None
165 self.__projectSeparatorActs = []
166
167 self.__projectBrowserMenu = None
168 self.__projectBrowserRawMetricsAct = None
169 self.__projectBrowserRawMetricsDialog = None
170 self.__projectBrowserSeparatorActs = []
171
172 self.__editors = []
173 self.__editorRawMetricsAct = None
174 self.__editorRawMetricsDialog = None
175 self.__editorSeparatorActs = []
176
177 def rawMetrics(self, lang, filename, source):
178 """
179 Public method to prepare raw code metrics calculation on one Python
180 source file.
181
182 @param lang language of the file or None to determine by internal
183 algorithm
184 @type str or None
185 @param filename source filename
186 @type str
187 @param source string containing the code
188 @type str
189 """
190 if lang is None:
191 lang = 'Python{0}'.format(determinePythonVersion(filename, source))
192 if lang not in ['Python2', 'Python3']:
193 return
194
195 self.backgroundService.enqueueRequest(
196 'radon', lang, filename, [source, 'raw'])
197
198 def rawMetricsBatch(self, argumentsList):
199 """
200 Public method to prepare raw code metrics calculation on multiple
201 Python source files.
202
203 @param argumentsList list of arguments tuples with each tuple
204 containing filename and source
205 @type (str, str)
206 """
207 data = {
208 "Python2": [],
209 "Python3": [],
210 }
211 for filename, source in argumentsList:
212 lang = 'Python{0}'.format(determinePythonVersion(filename, source))
213 if lang not in ['Python2', 'Python3']:
214 continue
215 else:
216 data[lang].append((filename, source, 'raw'))
217
218 self.queuedBatches = []
219 for lang in ['Python2', 'Python3']:
220 if data[lang]:
221 self.queuedBatches.append(lang)
222 self.backgroundService.enqueueRequest('batch_radon', lang, "",
223 data[lang])
224 self.batchesFinished = False
225
226 def cancelIndentBatchCheck(self):
227 """
228 Public method to cancel all batch jobs.
229 """
230 for lang in ['Python2', 'Python3']:
231 self.backgroundService.requestCancel('batch_radon', lang)
232
233 def activate(self):
234 """
235 Public method to activate this plug-in.
236
237 @return tuple of None and activation status
238 @rtype (None, bool)
239 """
240 global error
241 error = "" # clear previous error
242
243 menu = e5App().getObject("Project").getMenu("Show")
244 if menu:
245 if not menu.isEmpty():
246 act = menu.addSeparator()
247 act.setText(self.tr("Radon"))
248 self.__projectSeparatorActs.append(act)
249 self.__projectRawMetricsAct = E5Action(
250 self.tr('Code Metrics'),
251 self.tr('Code &Metrics...'), 0, 0,
252 self, 'project_show_radon_raw')
253 self.__projectRawMetricsAct.setStatusTip(
254 self.tr('Show raw code metrics.'))
255 self.__projectRawMetricsAct.setWhatsThis(self.tr(
256 """<b>Code Metrics...</b>"""
257 """<p>This calculates raw code metrics of Python files"""
258 """ and shows the amount of lines of code, logical lines"""
259 """ of code, source lines of code, comment lines,"""
260 """ multi-line strings and blank lines.</p>"""
261 ))
262 self.__projectRawMetricsAct.triggered.connect(
263 self.__projectRawMetrics)
264 menu.addAction(self.__projectRawMetricsAct)
265 act = menu.addSeparator()
266 self.__projectSeparatorActs.append(act)
267
268 e5App().getObject("Project").addE5Actions([
269 self.__projectRawMetricsAct,
270 ])
271
272 act = QAction("Radon", self)
273 act.setSeparator(True)
274 self.__editorSeparatorActs.append(act)
275 act = QAction(self)
276 act.setSeparator(True)
277 self.__editorSeparatorActs.append(act)
278
279 self.__editorRawMetricsAct = E5Action(
280 self.tr('Code Metrics'),
281 self.tr('Code &Metrics...'), 0, 0,
282 self, "")
283 self.__editorRawMetricsAct.setWhatsThis(self.tr(
284 """<b>Code Metrics...</b>"""
285 """<p>This calculates raw code metrics of Python files"""
286 """ and shows the amount of lines of code, logical lines"""
287 """ of code, source lines of code, comment lines,"""
288 """ multi-line strings and blank lines.</p>"""
289 ))
290 self.__editorRawMetricsAct.triggered.connect(self.__editorRawMetrics)
291
292 e5App().getObject("Project").showMenu.connect(self.__projectShowMenu)
293 e5App().getObject("ProjectBrowser").getProjectBrowser("sources")\
294 .showMenu.connect(self.__projectBrowserShowMenu)
295 e5App().getObject("ViewManager").editorOpenedEd.connect(
296 self.__editorOpened)
297 e5App().getObject("ViewManager").editorClosedEd.connect(
298 self.__editorClosed)
299
300 for editor in e5App().getObject("ViewManager").getOpenEditors():
301 self.__editorOpened(editor)
302
303 return None, True
304
305 def deactivate(self):
306 """
307 Public method to deactivate this plug-in.
308 """
309 e5App().getObject("Project").showMenu.disconnect(
310 self.__projectShowMenu)
311 e5App().getObject("ProjectBrowser").getProjectBrowser("sources")\
312 .showMenu.disconnect(self.__projectBrowserShowMenu)
313 e5App().getObject("ViewManager").editorOpenedEd.disconnect(
314 self.__editorOpened)
315 e5App().getObject("ViewManager").editorClosedEd.disconnect(
316 self.__editorClosed)
317
318 menu = e5App().getObject("Project").getMenu("Show")
319 if menu:
320 for sep in self.__projectSeparatorActs:
321 menu.removeAction(sep)
322 menu.removeAction(self.__projectRawMetricsAct)
323 e5App().getObject("Project").removeE5Actions(
324 [self.__projectRawMetricsAct])
325
326 if self.__projectBrowserMenu:
327 for sep in self.__projectBrowserSeparatorActs:
328 self.__projectBrowserMenu.removeAction(sep)
329 if self.__projectBrowserRawMetricsAct:
330 self.__projectBrowserMenu.removeAction(
331 self.__projectBrowserRawMetricsAct)
332
333 for editor in self.__editors:
334 editor.showMenu.disconnect(self.__editorShowMenu)
335 menu = editor.getMenu("Show")
336 if menu is not None:
337 for sep in self.__editorSeparatorActs:
338 menu.removeAction(sep)
339 menu.removeAction(self.__editorRawMetricsAct)
340
341 self.__initialize()
342
343 def __loadTranslator(self):
344 """
345 Private method to load the translation file.
346 """
347 if self.__ui is not None:
348 loc = self.__ui.getLocale()
349 if loc and loc != "C":
350 locale_dir = os.path.join(
351 os.path.dirname(__file__), "RadonMetrics", "i18n")
352 translation = "radon_{0}".format(loc)
353 translator = QTranslator(None)
354 loaded = translator.load(translation, locale_dir)
355 if loaded:
356 self.__translator = translator
357 e5App().installTranslator(self.__translator)
358 else:
359 print("Warning: translation file '{0}' could not be"
360 " loaded.".format(translation))
361 print("Using default.")
362
363 def __projectShowMenu(self, menuName, menu):
364 """
365 Private slot called, when the the project menu or a submenu is
366 about to be shown.
367
368 @param menuName name of the menu to be shown
369 @type str
370 @param menu reference to the menu
371 @type QMenu
372 """
373 if menuName == "Show":
374 for act in [self.__projectRawMetricsAct]:
375 if act is not None:
376 act.setEnabled(
377 e5App().getObject("Project").getProjectLanguage() in
378 ["Python3", "Python2", "Python"])
379
380 def __projectBrowserShowMenu(self, menuName, menu):
381 """
382 Private slot called, when the the project browser context menu or a
383 submenu is about to be shown.
384
385 @param menuName name of the menu to be shown (string)
386 @param menu reference to the menu (QMenu)
387 """
388 if menuName == "Show" and \
389 e5App().getObject("Project").getProjectLanguage() in \
390 ["Python3", "Python2", "Python"]:
391 if self.__projectBrowserMenu is None:
392 self.__projectBrowserMenu = menu
393 act = menu.addSeparator()
394 act.setText(self.tr("Radon"))
395 self.__projectBrowserSeparatorActs.append(act)
396
397 self.__projectBrowserRawMetricsAct = E5Action(
398 self.tr('Code Metrics'),
399 self.tr('Code &Metrics...'), 0, 0,
400 self, '')
401 self.__projectBrowserRawMetricsAct.setStatusTip(
402 self.tr('Show raw code metrics.'))
403 self.__projectBrowserRawMetricsAct.setWhatsThis(self.tr(
404 """<b>Code Metrics...</b>"""
405 """<p>This calculates raw code metrics of Python files"""
406 """ and shows the amount of lines of code, logical lines"""
407 """ of code, source lines of code, comment lines,"""
408 """ multi-line strings and blank lines.</p>"""
409 ))
410 self.__projectBrowserRawMetricsAct.triggered.connect(
411 self.__projectBrowserRawMetrics)
412 menu.addAction(self.__projectBrowserRawMetricsAct)
413
414 def __projectRawMetrics(self):
415 """
416 Private slot used to calculate raw code metrics for the project.
417 """
418 project = e5App().getObject("Project")
419 project.saveAllScripts()
420 ppath = project.getProjectPath()
421 files = [os.path.join(ppath, file)
422 for file in project.pdata["SOURCES"]
423 if file.endswith(
424 tuple(Preferences.getPython("Python3Extensions")) +
425 tuple(Preferences.getPython("PythonExtensions")))]
426
427 # TODO: implement this dialog
428 from RadonMetrics.RawMetricsDialog import RawMetricsDialog
429 self.__projectRawMetricsDialog = RawMetricsDialog(self)
430 self.__projectRawMetricsDialog.show()
431 self.__projectRawMetricsDialog.prepare(files, project)
432
433 def __projectBrowserRawMetrics(self):
434 """
435 Private method to handle the tabnanny context menu action of the
436 project sources browser.
437 """
438 browser = e5App().getObject("ProjectBrowser").getProjectBrowser(
439 "sources")
440 if browser.getSelectedItemsCount([ProjectBrowserFileItem]) > 1:
441 fn = []
442 for itm in browser.getSelectedItems([ProjectBrowserFileItem]):
443 fn.append(itm.fileName())
444 else:
445 itm = browser.model().item(browser.currentIndex())
446 try:
447 fn = itm.fileName()
448 except AttributeError:
449 fn = itm.dirName()
450
451 from RadonMetrics.RawMetricsDialog import RawMetricsDialog
452 self.__projectBrowserRawMetricsDialog = RawMetricsDialog(self)
453 self.__projectBrowserRawMetricsDialog.show()
454 self.__projectBrowserRawMetricsDialog.start(fn)
455
456 def __editorOpened(self, editor):
457 """
458 Private slot called, when a new editor was opened.
459
460 @param editor reference to the new editor
461 @type QScintilla.Editor
462 """
463 menu = editor.getMenu("Show")
464 if menu is not None:
465 menu.addAction(self.__editorSeparatorActs[0])
466 menu.addAction(self.__editorRawMetricsAct)
467 menu.addAction(self.__editorSeparatorActs[1])
468 editor.showMenu.connect(self.__editorShowMenu)
469 self.__editors.append(editor)
470
471 def __editorClosed(self, editor):
472 """
473 Private slot called, when an editor was closed.
474
475 @param editor reference to the editor (QScintilla.Editor)
476 """
477 try:
478 self.__editors.remove(editor)
479 except ValueError:
480 pass
481
482 def __editorShowMenu(self, menuName, menu, editor):
483 """
484 Private slot called, when the the editor context menu or a submenu is
485 about to be shown.
486
487 @param menuName name of the menu to be shown (string)
488 @param menu reference to the menu (QMenu)
489 @param editor reference to the editor
490 """
491 if menuName == "Show":
492 self.__editorRawMetricsAct.setEnabled(editor.isPyFile())
493
494 def __editorRawMetrics(self):
495 """
496 Private slot to handle the raw code metrics action of the editor show
497 menu.
498 """
499 editor = e5App().getObject("ViewManager").activeWindow()
500 if editor is not None:
501 if editor.checkDirty() and editor.getFileName() is not None:
502 from RadonMetrics.RawMetricsDialog import RawMetricsDialog
503 self.__editorRawMetricsDialog = RawMetricsDialog(self)
504 self.__editorRawMetricsDialog.show()
505 self.__editorRawMetricsDialog.start(editor.getFileName())

eric ide

mercurial