|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2023 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing an interface to the 'circup' package. |
|
8 """ |
|
9 |
|
10 import importlib |
|
11 import logging |
|
12 import os |
|
13 import re |
|
14 import shutil |
|
15 |
|
16 import requests |
|
17 |
|
18 from PyQt6.QtCore import QObject, pyqtSlot |
|
19 from PyQt6.QtWidgets import QDialog, QInputDialog, QLineEdit, QMenu |
|
20 |
|
21 from eric7 import Preferences |
|
22 from eric7.EricGui.EricOverrideCursor import EricOverrideCursor |
|
23 from eric7.EricWidgets import EricFileDialog, EricMessageBox |
|
24 from eric7.EricWidgets.EricApplication import ericApp |
|
25 from eric7.EricWidgets.EricListSelectionDialog import EricListSelectionDialog |
|
26 from eric7.SystemUtilities import PythonUtilities |
|
27 |
|
28 try: |
|
29 import circup |
|
30 |
|
31 circup.logger.setLevel(logging.WARNING) |
|
32 except ImportError: |
|
33 circup = None |
|
34 |
|
35 |
|
36 class CircuitPythonUpdaterInterface(QObject): |
|
37 """ |
|
38 Class implementing an interface to the 'circup' package. |
|
39 """ |
|
40 |
|
41 def __init__(self, device, parent=None): |
|
42 """ |
|
43 Constructor |
|
44 |
|
45 @param device reference to the CircuitPython device interface |
|
46 @type CircuitPythonDevice |
|
47 @param parent reference to the parent object (defaults to None) |
|
48 @type QObject (optional) |
|
49 """ |
|
50 super().__init__(parent) |
|
51 |
|
52 self.__device = device |
|
53 |
|
54 self.__installMenu = QMenu(self.tr("Install Modules")) |
|
55 self.__installMenu.setTearOffEnabled(True) |
|
56 self.__installMenu.addAction( |
|
57 self.tr("Select from Available Modules"), self.__installFromAvailable |
|
58 ) |
|
59 self.__installMenu.addAction( |
|
60 self.tr("Install Requirements"), self.__installRequirements |
|
61 ) |
|
62 self.__installMenu.addAction( |
|
63 self.tr("Install based on 'code.py'"), self.__installFromCode |
|
64 ) |
|
65 self.__installMenu.addSeparator() |
|
66 self.__installPyAct = self.__installMenu.addAction( |
|
67 self.tr("Install Python Source") |
|
68 ) |
|
69 self.__installPyAct.setCheckable(True) |
|
70 self.__installPyAct.setChecked(False) |
|
71 # kind of hack to make this action not hide the menu |
|
72 # Note: parent menus are hidden nevertheless |
|
73 self.__installPyAct.toggled.connect(self.__installMenu.show) |
|
74 |
|
75 def populateMenu(self, menu): |
|
76 """ |
|
77 Public method to populate the 'circup' menu. |
|
78 |
|
79 @param menu reference to the menu to be populated |
|
80 @type QMenu |
|
81 """ |
|
82 from .CircupFunctions import patch_circup |
|
83 |
|
84 patch_circup() |
|
85 |
|
86 act = menu.addAction(self.tr("circup"), self.__aboutCircup) |
|
87 font = act.font() |
|
88 font.setBold(True) |
|
89 act.setFont(font) |
|
90 menu.addSeparator() |
|
91 menu.addAction(self.tr("List Outdated Modules"), self.__listOutdatedModules) |
|
92 menu.addAction(self.tr("Update Modules"), self.__updateModules) |
|
93 menu.addAction(self.tr("Update All Modules"), self.__updateAllModules) |
|
94 menu.addSeparator() |
|
95 menu.addAction(self.tr("Show Available Modules"), self.__showAvailableModules) |
|
96 menu.addAction(self.tr("Show Installed Modules"), self.__showInstalledModules) |
|
97 menu.addMenu(self.__installMenu) |
|
98 menu.addAction(self.tr("Uninstall Modules"), self.__uninstallModules) |
|
99 menu.addSeparator() |
|
100 menu.addAction( |
|
101 self.tr("Generate Requirements ..."), self.__generateRequirements |
|
102 ) |
|
103 menu.addSeparator() |
|
104 menu.addAction(self.tr("Show Bundles"), self.__showBundles) |
|
105 menu.addAction(self.tr("Show Bundles with Modules"), self.__showBundlesModules) |
|
106 menu.addSeparator() |
|
107 menu.addAction(self.tr("Add Bundle"), self.__addBundle) |
|
108 menu.addAction(self.tr("Remove Bundles"), self.__removeBundle) |
|
109 |
|
110 @pyqtSlot() |
|
111 def __aboutCircup(self): |
|
112 """ |
|
113 Private slot to show some info about 'circup'. |
|
114 """ |
|
115 version = circup.get_circup_version() |
|
116 if version is None: |
|
117 version = self.tr("unknown") |
|
118 |
|
119 EricMessageBox.information( |
|
120 None, |
|
121 self.tr("About circup"), |
|
122 self.tr( |
|
123 """<p><b>circup Version {0}</b></p>""" |
|
124 """<p><i>circup</i> is a tool to manage and update libraries on a""" |
|
125 """ CircuitPython device.</p>""", |
|
126 ).format(version), |
|
127 ) |
|
128 |
|
129 @pyqtSlot() |
|
130 def installCircup(self): |
|
131 """ |
|
132 Public slot to install the 'circup' package via pip. |
|
133 """ |
|
134 global circup |
|
135 |
|
136 pip = ericApp().getObject("Pip") |
|
137 pip.installPackages( |
|
138 ["circup"], interpreter=PythonUtilities.getPythonExecutable() |
|
139 ) |
|
140 |
|
141 circup = importlib.import_module("circup") |
|
142 circup.logger.setLevel(logging.WARNING) |
|
143 |
|
144 @pyqtSlot() |
|
145 def __showBundles(self, withModules=False): |
|
146 """ |
|
147 Private slot to show the available bundles (default and local). |
|
148 |
|
149 @param withModules flag indicating to list the modules and their version |
|
150 (defaults to False) |
|
151 @type bool (optional) |
|
152 """ |
|
153 from .ShowBundlesDialog import ShowBundlesDialog |
|
154 |
|
155 with EricOverrideCursor(): |
|
156 dlg = ShowBundlesDialog(withModules=withModules) |
|
157 dlg.exec() |
|
158 |
|
159 @pyqtSlot() |
|
160 def __showBundlesModules(self): |
|
161 """ |
|
162 Private slot to show the available bundles (default and local) with their |
|
163 modules. |
|
164 """ |
|
165 self.__showBundles(withModules=True) |
|
166 |
|
167 @pyqtSlot() |
|
168 def __addBundle(self): |
|
169 """ |
|
170 Private slot to add a bundle to the local bundles list, by "user/repo" github |
|
171 string. |
|
172 """ |
|
173 bundle, ok = QInputDialog.getText( |
|
174 None, |
|
175 self.tr("Add Bundle"), |
|
176 self.tr("Enter Bundle by 'User/Repo' Github String:"), |
|
177 QLineEdit.EchoMode.Normal, |
|
178 ) |
|
179 if ok and bundle: |
|
180 bundles = circup.get_bundles_local_dict() |
|
181 modified = False |
|
182 |
|
183 # do some cleanup |
|
184 bundle = re.sub(r"https?://github.com/([^/]+/[^/]+)(/.*)?", r"\1", bundle) |
|
185 if bundle in bundles: |
|
186 EricMessageBox.information( |
|
187 None, |
|
188 self.tr("Add Bundle"), |
|
189 self.tr( |
|
190 """<p>The bundle <b>{0}</b> is already in the list.</p>""" |
|
191 ).format(bundle), |
|
192 ) |
|
193 return |
|
194 |
|
195 try: |
|
196 cBundle = circup.Bundle(bundle) |
|
197 except ValueError: |
|
198 EricMessageBox.critical( |
|
199 None, |
|
200 self.tr("Add Bundle"), |
|
201 self.tr( |
|
202 """<p>The bundle string is invalid, expecting github URL""" |
|
203 """ or 'user/repository' string.</p>""" |
|
204 ), |
|
205 ) |
|
206 return |
|
207 |
|
208 result = requests.head("https://github.com/" + bundle) |
|
209 if result.status_code == requests.codes.NOT_FOUND: |
|
210 EricMessageBox.critical( |
|
211 None, |
|
212 self.tr("Add Bundle"), |
|
213 self.tr( |
|
214 """<p>The bundle string is invalid. The repository doesn't""" |
|
215 """ exist (error code 404).</p>""" |
|
216 ), |
|
217 ) |
|
218 return |
|
219 |
|
220 if not cBundle.validate(): |
|
221 EricMessageBox.critical( |
|
222 None, |
|
223 self.tr("Add Bundle"), |
|
224 self.tr( |
|
225 """<p>The bundle string is invalid. Is the repository a valid""" |
|
226 """circup bundle?</p>""" |
|
227 ), |
|
228 ) |
|
229 return |
|
230 |
|
231 # Use the bundle string as the dictionary key for uniqueness |
|
232 bundles[bundle] = bundle |
|
233 modified = True |
|
234 EricMessageBox.information( |
|
235 None, |
|
236 self.tr("Add Bundle"), |
|
237 self.tr("""<p>Added bundle <b>{0}</b> ({1}).</p>""").format( |
|
238 bundle, cBundle.url |
|
239 ), |
|
240 ) |
|
241 |
|
242 if modified: |
|
243 # save the bundles list |
|
244 circup.save_local_bundles(bundles) |
|
245 # update and get the new bundle for the first time |
|
246 circup.get_bundle_versions(circup.get_bundles_list()) |
|
247 |
|
248 @pyqtSlot() |
|
249 def __removeBundle(self): |
|
250 """ |
|
251 Private slot to remove one or more bundles from the local bundles list. |
|
252 """ |
|
253 localBundles = circup.get_bundles_local_dict() |
|
254 dlg = EricListSelectionDialog( |
|
255 sorted(localBundles.keys()), |
|
256 title=self.tr("Remove Bundles"), |
|
257 message=self.tr("Select the bundles to be removed:"), |
|
258 checkBoxSelection=True, |
|
259 ) |
|
260 modified = False |
|
261 if dlg.exec() == QDialog.DialogCode.Accepted: |
|
262 bundles = dlg.getSelection() |
|
263 for bundle in bundles: |
|
264 del localBundles[bundle] |
|
265 modified = True |
|
266 |
|
267 if modified: |
|
268 circup.save_local_bundles(localBundles) |
|
269 EricMessageBox.information( |
|
270 None, |
|
271 self.tr("Remove Bundles"), |
|
272 self.tr( |
|
273 """<p>These bundles were removed from the local bundles list.{0}""" |
|
274 """</p>""" |
|
275 ).format("""<ul><li>{0}</li></ul>""".format("</li><li>".join(bundles))), |
|
276 ) |
|
277 |
|
278 @pyqtSlot() |
|
279 def __listOutdatedModules(self): |
|
280 """ |
|
281 Private slot to list the outdated modules of the connected device. |
|
282 """ |
|
283 from .ShowOutdatedDialog import ShowOutdatedDialog |
|
284 |
|
285 devicePath = self.__device.getWorkspace() |
|
286 |
|
287 cpyVersion, board_id = circup.get_circuitpython_version(devicePath) |
|
288 circup.CPY_VERSION = cpyVersion |
|
289 |
|
290 with EricOverrideCursor(): |
|
291 dlg = ShowOutdatedDialog(devicePath=devicePath) |
|
292 dlg.exec() |
|
293 |
|
294 @pyqtSlot() |
|
295 def __updateModules(self): |
|
296 """ |
|
297 Private slot to update the modules of the connected device. |
|
298 """ |
|
299 from .ShowOutdatedDialog import ShowOutdatedDialog |
|
300 |
|
301 devicePath = self.__device.getWorkspace() |
|
302 |
|
303 cpyVersion, board_id = circup.get_circuitpython_version(devicePath) |
|
304 circup.CPY_VERSION = cpyVersion |
|
305 |
|
306 with EricOverrideCursor(): |
|
307 dlg = ShowOutdatedDialog(devicePath=devicePath, selectionMode=True) |
|
308 if dlg.exec() == QDialog.DialogCode.Accepted: |
|
309 modules = dlg.getSelection() |
|
310 self.__doUpdateModules(modules) |
|
311 |
|
312 @pyqtSlot() |
|
313 def __updateAllModules(self): |
|
314 """ |
|
315 Private slot to update all modules of the connected device. |
|
316 """ |
|
317 devicePath = self.__device.getWorkspace() |
|
318 |
|
319 cpyVersion, board_id = circup.get_circuitpython_version(devicePath) |
|
320 circup.CPY_VERSION = cpyVersion |
|
321 |
|
322 with EricOverrideCursor(): |
|
323 modules = [ |
|
324 m |
|
325 for m in circup.find_modules(devicePath, circup.get_bundles_list()) |
|
326 if m.outofdate |
|
327 ] |
|
328 if modules: |
|
329 self.__doUpdateModules(modules) |
|
330 else: |
|
331 EricMessageBox.information( |
|
332 None, |
|
333 self.tr("Update Modules"), |
|
334 self.tr("All modules are already up-to-date."), |
|
335 ) |
|
336 |
|
337 def __doUpdateModules(self, modules): |
|
338 """ |
|
339 Private method to perform the update of a list of modules. |
|
340 |
|
341 @param modules list of modules to be updated |
|
342 @type circup.Module |
|
343 """ |
|
344 updatedModules = [] |
|
345 for module in modules: |
|
346 try: |
|
347 module.update() |
|
348 updatedModules.append(module.name) |
|
349 except Exception as ex: |
|
350 EricMessageBox.critical( |
|
351 None, |
|
352 self.tr("Update Modules"), |
|
353 self.tr( |
|
354 """<p>There was an error updating <b>{0}</b>.</p>""" |
|
355 """<p>Error: {1}</p>""" |
|
356 ).format(module.name, str(ex)), |
|
357 ) |
|
358 |
|
359 if updatedModules: |
|
360 EricMessageBox.information( |
|
361 None, |
|
362 self.tr("Update Modules"), |
|
363 self.tr( |
|
364 """<p>These modules were updated on the connected device.{0}</p>""" |
|
365 ).format( |
|
366 """<ul><li>{0}</li></ul>""".format("</li><li>".join(updatedModules)) |
|
367 ), |
|
368 ) |
|
369 else: |
|
370 EricMessageBox.information( |
|
371 None, |
|
372 self.tr("Update Modules"), |
|
373 self.tr("No modules could be updated."), |
|
374 ) |
|
375 |
|
376 @pyqtSlot() |
|
377 def __showAvailableModules(self): |
|
378 """ |
|
379 Private slot to show the available modules. |
|
380 |
|
381 These are modules which could be installed on the device. |
|
382 """ |
|
383 from .ShowModulesDialog import ShowModulesDialog |
|
384 |
|
385 with EricOverrideCursor(): |
|
386 dlg = ShowModulesDialog() |
|
387 dlg.exec() |
|
388 |
|
389 @pyqtSlot() |
|
390 def __showInstalledModules(self): |
|
391 """ |
|
392 Private slot to show the modules installed on the connected device. |
|
393 """ |
|
394 from .ShowInstalledDialog import ShowInstalledDialog |
|
395 |
|
396 devicePath = self.__device.getWorkspace() |
|
397 |
|
398 with EricOverrideCursor(): |
|
399 dlg = ShowInstalledDialog(devicePath=devicePath) |
|
400 dlg.exec() |
|
401 |
|
402 @pyqtSlot() |
|
403 def __installFromAvailable(self): |
|
404 """ |
|
405 Private slot to install modules onto the connected device. |
|
406 """ |
|
407 from .ShowModulesDialog import ShowModulesDialog |
|
408 |
|
409 with EricOverrideCursor(): |
|
410 dlg = ShowModulesDialog(selectionMode=True) |
|
411 if dlg.exec() == QDialog.DialogCode.Accepted: |
|
412 modules = dlg.getSelection() |
|
413 self.__installModules(modules) |
|
414 |
|
415 @pyqtSlot() |
|
416 def __installRequirements(self): |
|
417 """ |
|
418 Private slot to install modules determined by a requirements file. |
|
419 """ |
|
420 homeDir = ( |
|
421 Preferences.getMicroPython("MpyWorkspace") |
|
422 or Preferences.getMultiProject("Workspace") |
|
423 or os.path.expanduser("~") |
|
424 ) |
|
425 reqFile = EricFileDialog.getOpenFileName( |
|
426 None, |
|
427 self.tr("Install Modules"), |
|
428 homeDir, |
|
429 self.tr("Text Files (*.txt);;All Files (*)"), |
|
430 ) |
|
431 if reqFile: |
|
432 if os.path.exists(reqFile): |
|
433 with open(reqFile, "r") as fp: |
|
434 requirementsText = fp.read() |
|
435 modules = circup.libraries_from_requirements(requirementsText) |
|
436 if modules: |
|
437 self.__installModules(modules) |
|
438 else: |
|
439 EricMessageBox.critical( |
|
440 None, |
|
441 self.tr("Install Modules"), |
|
442 self.tr( |
|
443 """<p>The given requirements file <b>{0}</b> does not""" |
|
444 """ contain valid modules.</p>""" |
|
445 ).format(reqFile), |
|
446 ) |
|
447 else: |
|
448 EricMessageBox.critical( |
|
449 None, |
|
450 self.tr("Install Modules"), |
|
451 self.tr( |
|
452 """<p>The given requirements file <b>{0}</b> does not exist.""" |
|
453 """</p>""" |
|
454 ).format(reqFile), |
|
455 ) |
|
456 |
|
457 @pyqtSlot() |
|
458 def __installFromCode(self): |
|
459 """ |
|
460 Private slot to install modules based on the 'code.py' file of the |
|
461 connected device. |
|
462 """ |
|
463 devicePath = self.__device.getWorkspace() |
|
464 |
|
465 codeFile = EricFileDialog.getOpenFileName( |
|
466 None, |
|
467 self.tr("Install Modules"), |
|
468 os.path.join(devicePath, "code.py"), |
|
469 self.tr("Python Files (*.py);;All Files (*)"), |
|
470 ) |
|
471 if codeFile: |
|
472 if os.path.exists(codeFile): |
|
473 |
|
474 with EricOverrideCursor(): |
|
475 availableModules = circup.get_bundle_versions( |
|
476 circup.get_bundles_list() |
|
477 ) |
|
478 moduleNames = {} |
|
479 for module, metadata in availableModules.items(): |
|
480 moduleNames[module.replace(".py", "")] = metadata |
|
481 |
|
482 modules = circup.libraries_from_imports(codeFile, moduleNames) |
|
483 if modules: |
|
484 self.__installModules(modules) |
|
485 else: |
|
486 EricMessageBox.critical( |
|
487 None, |
|
488 self.tr("Install Modules"), |
|
489 self.tr( |
|
490 """<p>The given code file <b>{0}</b> does not""" |
|
491 """ contain valid import statements or does not import""" |
|
492 """ external modules.</p>""" |
|
493 ).format(codeFile), |
|
494 ) |
|
495 else: |
|
496 EricMessageBox.critical( |
|
497 None, |
|
498 self.tr("Install Modules"), |
|
499 self.tr( |
|
500 """<p>The given code file <b>{0}</b> does not exist.</p>""" |
|
501 ).format(codeFile), |
|
502 ) |
|
503 |
|
504 def __installModules(self, installs): |
|
505 """ |
|
506 Private method to install the given list of modules. |
|
507 |
|
508 @param installs list of module names to be installed |
|
509 @type list of str |
|
510 """ |
|
511 devicePath = self.__device.getWorkspace() |
|
512 |
|
513 cpyVersion, board_id = circup.get_circuitpython_version(devicePath) |
|
514 circup.CPY_VERSION = cpyVersion |
|
515 |
|
516 with EricOverrideCursor(): |
|
517 availableModules = circup.get_bundle_versions(circup.get_bundles_list()) |
|
518 moduleNames = {} |
|
519 for module, metadata in availableModules.items(): |
|
520 moduleNames[module.replace(".py", "")] = metadata |
|
521 toBeInstalled = circup.get_dependencies(installs, mod_names=moduleNames) |
|
522 deviceModules = circup.get_device_versions(devicePath) |
|
523 if toBeInstalled is not None: |
|
524 dependencies = [m for m in toBeInstalled if m not in installs] |
|
525 ok = EricMessageBox.yesNo( |
|
526 None, |
|
527 self.tr("Install Modules"), |
|
528 self.tr("""<p>Ready to install these modules?{0}{1}</p>""").format( |
|
529 """<ul><li>{0}</li></ul>""".format( |
|
530 "</li><li>".join(sorted(installs)) |
|
531 ), |
|
532 self.tr("Dependencies:") |
|
533 + """<ul><li>{0}</li></ul>""".format( |
|
534 "</li><li>".join(sorted(dependencies)) |
|
535 ) |
|
536 if dependencies |
|
537 else "", |
|
538 ), |
|
539 yesDefault=True, |
|
540 ) |
|
541 if ok: |
|
542 installedModules = [] |
|
543 with EricOverrideCursor(): |
|
544 for library in toBeInstalled: |
|
545 success = circup.install_module( |
|
546 devicePath, |
|
547 deviceModules, |
|
548 library, |
|
549 self.__installPyAct.isChecked(), |
|
550 moduleNames, |
|
551 ) |
|
552 if success: |
|
553 installedModules.append(library) |
|
554 |
|
555 if installedModules: |
|
556 EricMessageBox.information( |
|
557 None, |
|
558 self.tr("Install Modules"), |
|
559 self.tr( |
|
560 "<p>Installation complete. These modules were installed" |
|
561 " successfully.{0}</p>" |
|
562 ).format( |
|
563 """<ul><li>{0}</li></ul>""".format( |
|
564 "</li><li>".join(sorted(installedModules)) |
|
565 ), |
|
566 ), |
|
567 ) |
|
568 else: |
|
569 EricMessageBox.information( |
|
570 None, |
|
571 self.tr("Install Modules"), |
|
572 self.tr( |
|
573 "<p>Installation complete. No modules were installed.</p>" |
|
574 ), |
|
575 ) |
|
576 else: |
|
577 EricMessageBox.information( |
|
578 None, |
|
579 self.tr("Install Modules"), |
|
580 self.tr("<p>No modules installation is required.</p>"), |
|
581 ) |
|
582 |
|
583 @pyqtSlot() |
|
584 def __uninstallModules(self): |
|
585 """ |
|
586 Private slot to uninstall modules from the connected device. |
|
587 """ |
|
588 devicePath = self.__device.getWorkspace() |
|
589 libraryPath = os.path.join(devicePath, "lib") |
|
590 |
|
591 with EricOverrideCursor(): |
|
592 deviceModules = circup.get_device_versions(devicePath) |
|
593 modNames = {} |
|
594 for moduleItem, metadata in deviceModules.items(): |
|
595 modNames[moduleItem.replace(".py", "").lower()] = metadata |
|
596 |
|
597 dlg = EricListSelectionDialog( |
|
598 sorted(modNames.keys()), |
|
599 title=self.tr("Uninstall Modules"), |
|
600 message=self.tr("Select the modules/packages to be uninstalled:"), |
|
601 checkBoxSelection=True, |
|
602 ) |
|
603 if dlg.exec() == QDialog.DialogCode.Accepted: |
|
604 names = dlg.getSelection() |
|
605 for name in names: |
|
606 modulePath = modNames[name]["path"] |
|
607 if os.path.isdir(modulePath): |
|
608 target = os.path.basename(os.path.dirname(modulePath)) |
|
609 targetPath = os.path.join(libraryPath, target) |
|
610 # Remove the package directory. |
|
611 shutil.rmtree(targetPath) |
|
612 else: |
|
613 target = os.path.basename(modulePath) |
|
614 targetPath = os.path.join(libraryPath, target) |
|
615 # Remove the module file |
|
616 os.remove(targetPath) |
|
617 |
|
618 EricMessageBox.information( |
|
619 None, |
|
620 self.tr("Uninstall Modules"), |
|
621 self.tr( |
|
622 """<p>These modules/packages were uninstalled from the connected""" |
|
623 """ device.{0}</p>""" |
|
624 ).format("""<ul><li>{0}</li></ul>""".format("</li><li>".join(names))), |
|
625 ) |
|
626 |
|
627 @pyqtSlot() |
|
628 def __generateRequirements(self): |
|
629 """ |
|
630 Private slot to generate requirements for the connected device. |
|
631 """ |
|
632 from .RequirementsDialog import RequirementsDialog |
|
633 |
|
634 devicePath = self.__device.getWorkspace() |
|
635 |
|
636 cpyVersion, board_id = circup.get_circuitpython_version(devicePath) |
|
637 circup.CPY_VERSION = cpyVersion |
|
638 |
|
639 dlg = RequirementsDialog(devicePath=devicePath) |
|
640 dlg.exec() |
|
641 |
|
642 |
|
643 def isCircupAvailable(): |
|
644 """ |
|
645 Function to check for the availability of 'circup'. |
|
646 |
|
647 @return flag indicating the availability of 'circup' |
|
648 @rtype bool |
|
649 """ |
|
650 global circup |
|
651 |
|
652 return circup is not None |