Package empro :: Package toolkit :: Module addon
[frames] | no frames]

Source Code for Module empro.toolkit.addon

  1  # Copyright 1983-2019 Keysight Technologies, Inc , Keysight Confidential 
  2   
  3  import ast 
  4  import os 
  5  import sys 
  6  import traceback 
  7  import types 
  8   
  9  import empro 
 10  from empro.toolkit import _printexception 
11 12 13 -class AddonDefinition(object):
14 - def __init__(self, menuItem=None, onContextMenu=None):
15 self.menuItem = menuItem 16 self.onContextMenu = onContextMenu
17
18 19 -def makeAction(title, onTriggered, icon=None):
20 ''' 21 Helper function to wrap a Python function into a gui.Action. 22 - title: string (which can contain ampersands for shortcuts) 23 - onTriggered: Python function that does not require arguments 24 - icon: optional, either an gui.Icon, or a file path to an icon image. 25 ''' 26 from empro import gui 27 def func(checked): 28 try: 29 try: 30 onTriggered() 31 except: 32 gui.MessageBox.critical( "Add-on Error", u"A fatal error occurred while executing add-on call:\n\n%s" % traceback.format_exc(), gui.Ok, gui.Ok ) 33 except: 34 traceback.print_exc()
35 action = gui.Action(title) 36 action.onTriggered = func 37 if isinstance(icon, gui.Icon): 38 action.icon = icon 39 elif icon: 40 action.icon = gui.Icon(icon) 41 return action 42
43 44 -def makeContextAction(title, onTriggered, icon=None):
45 ''' 46 Helper function to wrap a Python function into a gui.Action for a context menu 47 - title: string (which can contain ampersands for shortcuts) 48 - onTriggered: Python function that does require one argument: the context selection. 49 - icon: optional, either an gui.Icon, or a file path to an icon image. 50 ''' 51 from empro import gui 52 def func(checked): 53 try: 54 selection = gui.SelectionList.globalSelectionList().selection() 55 try: 56 onTriggered(selection) 57 except: 58 gui.MessageBox.critical( "Add-on Error", u"A fatal error occurred while executing add-on call:\n\n%s" % traceback.format_exc(), gui.Ok, gui.Ok ) 59 except: 60 traceback.print_exc()
61 action = gui.Action(title) 62 action.onTriggered = func 63 if isinstance(icon, gui.Icon): 64 action.icon = icon 65 elif icon: 66 action.icon = gui.Icon(icon) 67 return action 68
69 70 -def loadAdditionalAddons(searchDirs, forceEnabled=True):
71 ''' 72 Search additional add-ons in searchDirs, and load them. 73 searchDirs can be a string or list of directories. 74 When forceEnabled is True, the new-found add-ons will be enabled regardless 75 of the user settings. 76 77 Add-ons loaded before calling loadAdditionalAddons can influence the 78 behaviour of this call. If a add-on by the same name but different 79 location is already loaded, the new one will be ignored. 80 ''' 81 addons = _searchAddons(searchDirs, forceEnabled=forceEnabled) 82 _loadAddons(addons) 83 return addons
84
85 86 # --- implementation --- 87 88 -def _init():
89 addons = _searchAddons() 90 _loadAddons(addons)
91
92 93 -def _searchAddons(searchDirs=None, forceEnabled=False):
94 if not searchDirs: 95 searchDirs = (_additionalSearchPath().split(os.pathsep) + 96 _platformSearchDirs() + 97 _extraPlatformSearchDirs()) 98 if isinstance(searchDirs, basestring): 99 searchDirs = searchDirs.split(os.pathsep) 100 101 try: 102 empro.internal._addons 103 except AttributeError: 104 empro.internal._addons = {} 105 106 addons = {} 107 for location in map(_normPath, searchDirs): 108 if not location or not os.path.isdir(location): 109 continue 110 111 for fname in os.listdir(location): 112 if fname.startswith('_'): 113 continue 114 name, ext = os.path.splitext(fname) 115 fpath = path = os.path.normpath(os.path.join(location, fname)) 116 if os.path.isdir(path): 117 # is it a package? Let path be the path to the package 118 # directory, but fpath the path to the __init__ file. 119 for ext in _SUFFIXES: 120 fpath = os.path.join(path, '__init__' + ext) 121 if os.path.isfile(fpath): 122 break # ext also set 123 else: 124 continue 125 assert os.path.isfile(fpath), fpath 126 if not ext in _SUFFIXES: 127 continue 128 129 try: 130 found = empro.internal._addons[name] 131 except KeyError: 132 pass 133 else: 134 if found.module or found.error: 135 continue # already loaded, can't override anymore. 136 if os.path.dirname(path) != os.path.dirname(found.path): 137 continue # add-on by same name found in other location, ignore duplicate. 138 try: 139 if _SUFFIXES.index(ext) >= _SUFFIXES.index(os.path.splitext(found.path)[1]): 140 continue # no .pyc if .py or .pyw already found. 141 except ValueError: 142 # ??? cf. DDR as directory 143 continue 144 145 addon = _loadAddonMetaData(path, fpath) 146 if not addon: 147 continue 148 addon.forceEnabled = addon.forceEnabled or forceEnabled 149 addons[name] = empro.internal._addons[name] = addon 150 151 return addons
152
153 154 -def _loadAddonMetaData(path, fpath):
155 addon = _AddonMetaData(path) 156 try: 157 with open(fpath) as fp: 158 source = fp.read() 159 module = ast.parse(source, filename=fpath) 160 # ensure we have a _defineAddon function 161 for node in module.body: 162 if isinstance(node, ast.FunctionDef) and node.name == '_defineAddon': 163 break 164 else: 165 return None # not a valid addon 166 167 addon.docstring = unwrapLines(ast.get_docstring(module)) 168 # scan for other metadata 169 fields = ('__author__', '__version__', '__personalities__') 170 for node in module.body: 171 if not isinstance(node, ast.Assign): 172 continue 173 if len(node.targets) != 1: 174 continue 175 try: 176 id = node.targets[0].id 177 except AttributeError: 178 continue 179 if not id in fields: 180 continue 181 try: 182 value = ast.literal_eval(node.value) 183 except ValueError: 184 continue 185 setattr(addon, id.strip('_'), value) 186 except: 187 addon.error = ''.join(traceback.format_exception_only(*sys.exc_info()[:2])).strip() 188 addon.traceback = traceback.format_exc().strip() 189 return addon
190
191 192 -def _loadAddons(addons):
193 if "--no-load-addons" in sys.argv: 194 return 195 196 personality = empro.core.ApplicationInfo.personality() 197 198 namespace = 'empro.addons' 199 try: 200 empro.addons = sys.modules[namespace] 201 except KeyError: 202 empro.addons = sys.modules[namespace] = types.ModuleType(namespace) 203 204 enabledAddons = _enabledAddons() 205 for (name, addon) in sorted(addons.items()): 206 defaultEnabled = os.path.dirname(addon.path) in _platformSearchDirs() 207 addon.enabled = addon.forceEnabled or enabledAddons.get(name, defaultEnabled) 208 209 if hasattr(addon,"personalities"): 210 211 if addon.personalities=="all" or "all" in addon.personalities: 212 pass 213 else: 214 if personality not in addon.personalities: 215 continue 216 217 218 addon.available = True 219 if not addon.enabled: 220 continue 221 222 if addon.module: 223 continue # already loaded 224 try: 225 fullName = "{}.{}".format(namespace, name) 226 addon.module = _importModule(fullName, addon.path) 227 setattr(empro.addons, name, addon.module) 228 addon.definition = addon.module._defineAddon() 229 except: 230 addon.error = ''.join(traceback.format_exception_only(*sys.exc_info()[:2])).strip() 231 addon.traceback = traceback.format_exc().strip() 232 continue 233 234 _makeAddonMenu(addons) 235 _prepareOnContextMenus(addons)
236
237 238 -def _importModule(name, path):
239 if not name in sys.modules: 240 sys.modules[name] = _loadModule(name, path) 241 return sys.modules[name]
242 243 244 try: 245 import importlib.machinery 246 import importlib.util 247 except ImportError: 248 # Python 2.7 compatibility 249 import imp 250 _SUFFIXES = [ 251 suffix for (suffix, m, t) in imp.get_suffixes() if t == imp.PY_SOURCE 252 ]
253 254 - def _loadModule(name, path):
255 imp.acquire_lock() 256 try: 257 basename = name.rsplit('.', 1)[-1] 258 f, modpath, desc = imp.find_module(basename, [os.path.dirname(path)]) 259 return imp.load_module(basename, f, modpath, desc) 260 finally: 261 imp.release_lock()
262 else: 263 _SUFFIXES = importlib.machinery.SOURCE_SUFFIXES
264 265 - def _loadModule(name, path):
266 # https://docs.python.org/3.7/library/importlib.html#importing-a-source-file-directly 267 spec = importlib.util.spec_from_file_location(name, path) 268 module = importlib.util.module_from_spec(spec) 269 spec.loader.exec_module(module) 270 return module
271
272 273 -def _makeAddonMenu(addons):
274 toolsMenu = empro.gui.activeProjectView().menu("tools") 275 276 personality = empro.core.ApplicationInfo.personality() 277 278 try: 279 menuItems, insertPoint = empro.internal._addonMenuRange 280 except AttributeError: 281 # we don't have this menu yet, let's make it. 282 oldTop = toolsMenu.actions()[0] 283 toolsMenu.insertSeparator(oldTop) 284 insertPoint = toolsMenu.insertSeparator(oldTop) 285 empro.internal._addonManager = toolsMenu.insertAction(oldTop, 286 makeAction('&Add-on Manager...', _showAddonManager, icon=':/workspace/ScriptWorkspaceWindow.ico')) 287 else: 288 # remove existing actions 289 for item in menuItems: 290 toolsMenu.removeAction(item) 291 _unregisterActions() 292 293 menuItems = [] 294 try: 295 for name, addon in sorted(addons.items()): 296 if not addon.enabled or not addon.definition: 297 continue 298 menuItem = addon.definition.menuItem 299 300 if isinstance(menuItem, empro.gui.Action): 301 menuItems.append(toolsMenu.insertAction(insertPoint, menuItem)) 302 _registerAction(menuItem) 303 elif isinstance(menuItem, empro.gui.Menu): 304 menuItems.append(toolsMenu.insertMenu(insertPoint, menuItem)) 305 _registerAction(menuItem) 306 finally: 307 empro.internal._addonMenuRange = menuItems, insertPoint
308
309 310 -def _prepareOnContextMenus(addons):
311 onContextMenus = [] 312 for (name, addon) in sorted(addons.items()): 313 if not addon.enabled or not addon.definition: 314 continue 315 if addon.definition.onContextMenu: 316 onContextMenus.append(addon.definition.onContextMenu) 317 empro.internal._addonOnContextMenus = onContextMenus
318
319 320 -class _AddonMetaData(object):
321 - def __init__(self, path, docstring=None, author=None, contact=None, version=None):
322 self.path = path 323 self.docstring = docstring 324 self.author = author 325 self.contact = contact 326 self.version = version 327 self.module = None 328 self.definition = None 329 self.error = None 330 self.traceback = None 331 self.enabled = False 332 self.personalities = ["empro"] 333 self.available = False 334 self.forceEnabled = False
335 336 337 _ADDON_DOWNLOAD_URL = "http://www.keysight.com/find/eesof-empro-addons"
338 339 -def _showAddonManager():
340 from empro import gui 341 342 # search default paths again to discover new addons ... 343 _searchAddons() 344 345 dialog = gui.SimpleDialog(gui.Ok | gui.Cancel) 346 appName = empro.core.ApplicationInfo.applicationName() 347 appName = appName.replace("Setup","") 348 dialog.title = "Keysight %s - Add-on Manager" % (appName) 349 dialog.windowFlags &= ~gui.WF_WindowStaysOnTopHint 350 dialog.resize(400, 500) 351 layout = dialog.layout 352 353 checkboxes = {} 354 enableds = _enabledAddons() 355 scrollArea = gui.ScrollArea() 356 scrollArea.widget = scrollWidget = gui.Frame() 357 scrollArea.widgetResizable = True 358 scrollArea.horizontalScrollBarPolicy = 1 359 layout.add(scrollArea) 360 scrollLayout = gui.VBoxLayout(scrollWidget) 361 362 toolButtons = [] 363 for (name, addon) in sorted(empro.internal._addons.items()): 364 if not addon.available: 365 continue 366 367 addonBox = gui.Frame() 368 addonBox.frameStyle = gui.Frame.StyledPanel 369 addonLayout = gui.GridLayout(addonBox) 370 371 if addon.error: 372 icon = gui.ValidityLabel(False, addon.traceback or addon.error) 373 elif addon.module: 374 icon = gui.ValidityLabel(True, "Loaded") 375 else: 376 icon = None 377 if icon: 378 addonLayout.addWidget( icon, 0, 0, 1, 1 ) 379 addonLayout.setAlignment( icon, gui.AlignHCenter ) 380 381 checkboxes[name] = checkbox = gui.CheckBox("%s" % os.path.basename(addon.path)) 382 checkbox.checked = enableds.get(name, addon.enabled) 383 checkbox.styleSheet = "QCheckBox { font: bold; }" 384 checkbox.toolTip = addon.path 385 checkbox.enabled = not addon.forceEnabled 386 addonLayout.addWidget( checkbox, 0, 1, 1, 2 ) 387 388 if addon.docstring: 389 description = gui.Label( addon.docstring.splitlines()[0] ) 390 description.wordWrap = True 391 addonLayout.addWidget( description, 1, 1, 1, 2 ) 392 fullDescription = gui.ToolButton() 393 fullDescription.icon = gui.Icon(":/application/Help.ico") 394 fullDescription.onClicked = lambda _, n=name : _showAddonInfo(n, empro.internal._addons[n]) 395 addonLayout.addWidget( fullDescription, 1, 0, 1, 1 ) 396 toolButtons.append(fullDescription) 397 if addon.author: 398 addonLayout.addWidget( gui.Label( "Author:" ), 2, 0, 1, 2 ) 399 addonLayout.addWidget( gui.Label( addon.author ), 2, 2, 1, 1 ) 400 if addon.version: 401 addonLayout.addWidget( gui.Label( "Version:" ), 3, 0, 1, 2 ) 402 addonLayout.addWidget( gui.Label( addon.version ), 3, 2, 1, 1 ) 403 addonLayout.setColumnStretch(2, 2) 404 405 scrollLayout.addWidget(addonBox) 406 407 scrollLayout.addStretch(1) 408 409 download = gui.Label('Additional add-ons can be downloaded from the <a href="%s">Knowledge Center</a> and saved on the search path:' % _ADDON_DOWNLOAD_URL) 410 download.openExternalLinks = True 411 download.wordWrap = True 412 layout.add(download) 413 414 additionalSearchPathEdit = gui.LineEdit(_additionalSearchPath()) 415 layout.add(additionalSearchPathEdit) 416 417 @_printexception 418 def onFinished(code): 419 if code != dialog.Accepted: 420 return 421 _additionalSearchPath(additionalSearchPathEdit.text) 422 _enabledAddons( { name: checkbox.checked for (name, checkbox) in checkboxes.items() } ) 423 _loadAddons(empro.internal._addons)
424 dialog.onFinished = onFinished 425 426 dialog.show(True) 427
428 429 -def _showAddonInfo(name, addon):
430 if not addon.docstring: 431 return 432 from empro import gui 433 dialog = gui.SimpleDialog(gui.Close) 434 dialog.resize(600, 300) 435 dialog.windowFlags &= ~gui.WF_WindowStaysOnTopHint 436 dialog.title = name or "Add-on" 437 doc = gui.TextEdit(addon.docstring) 438 doc.readOnly = True 439 doc.styleSheet = "QTextEdit { border: 0; }" 440 dialog.layout.add(doc) 441 dialog.show(True)
442
443 444 -def _addContextMenuItem( menu, menuItems, item ):
445 from empro import gui 446 if type( item ) is dict: 447 try: 448 menuItem = item["menuItem"] 449 insertionPoint = item["insertionPoint"] 450 except: 451 return 452 if type( insertionPoint ) is int: 453 if isinstance( menuItem, gui.Action ): 454 menu.insertAction( menu.actions()[insertionPoint], menuItem ) 455 elif isinstance( menuItem, gui.Menu ): 456 menu.insertMenu( menu.actions()[insertionPoint], menuItem ) 457 else: 458 return 459 menuItems.append( menuItem ) 460 else: 461 return 462 else: 463 if isinstance( item, gui.Action ): 464 menu.addAction( item ) 465 elif isinstance( item, gui.Menu ): 466 menu.addMenu( item ) 467 else: 468 return 469 menuItems.append( item )
470
471 472 -def _onContextMenu(menu, selection):
473 try: 474 onContextMenus = empro.internal._addonOnContextMenus 475 except AttributeError: 476 return 477 typeSet = frozenset(map(type, selection)) 478 menuItems = [] 479 menu.addSeparator() 480 for onContextMenu in onContextMenus: 481 try: 482 items = onContextMenu(selection, typeSet) 483 except: 484 continue 485 if type( items) is list: 486 for item in items: 487 _addContextMenuItem( menu, menuItems, item ) 488 else: 489 _addContextMenuItem( menu, menuItems, items ) # items is in fact a single value, i.e. a dict or an action 490 empro.internal._addonContextMenuItems = menuItems # store reference to keep them alive
491
492 493 -def _additionalSearchPath(newPath=None):
494 preference = "Addons/AdditionalSearchPath" 495 if not newPath is None: 496 empro.core.ApplicationPreferences.setPreference(preference, newPath) 497 default = os.path.join(empro.internal.documentsLocation(), "Keysight", "EMPro", "addons") 498 path = empro.core.ApplicationPreferences.getPreference(preference, default) 499 if path == default and not os.path.exists(default): 500 try: 501 os.makedirs(default) 502 except: 503 pass 504 return path
505
506 507 -def _platformSearchDirs():
508 try: 509 return [_normPath(os.path.join(os.environ['EMPROHOME'], os.pardir, os.pardir, "python_scripts", "addons"))] 510 except KeyError: 511 return []
512
513 514 -def _extraPlatformSearchDirs():
515 try: 516 return [_normPath(os.path.join(os.environ['EMPROHOME'], os.pardir, os.pardir, "python_scripts", "extra_addons"))] 517 except KeyError: 518 return []
519 520 521 _PREFERENCE_GROUP = "Addons"
522 523 -def _enabledAddons(newDict=None):
524 preferenceEnabled = "%s/Enabled" % _PREFERENCE_GROUP 525 preferenceDisabled = "%s/Disabled" % _PREFERENCE_GROUP 526 if not newDict is None: 527 empro.core.ApplicationPreferences.setPreference(preferenceEnabled, [name for name in newDict if newDict[name]]) 528 empro.core.ApplicationPreferences.setPreference(preferenceDisabled, [name for name in newDict if not newDict[name]]) 529 enableds = { name:False for name in empro.core.ApplicationPreferences.getPreference(preferenceDisabled, None) or [] } 530 enableds.update( { name:True for name in empro.core.ApplicationPreferences.getPreference(preferenceEnabled, None) or [] } ) 531 return enableds
532
533 534 -def _normPath(path):
535 if not path: 536 return path 537 return os.path.normpath(os.path.expandvars(os.path.expanduser((path))))
538
539 540 -def unwrapLines(docstring):
541 ''' 542 joins lines in a docstring where a newline appears to be in the middle 543 of a sentence. 544 545 It's not perfect, and the following rules apply for two lines to be joined: 546 - neither should be empty or all whitespace 547 - there should be no indentation. 548 - the second line should start with a letter (capital or small) 549 ''' 550 # docstrings already have had some processing (leading and trailing empty 551 # lines, and tabs to 8 spaces) See http://www.python.org/dev/peps/pep-0257/ 552 if not docstring: 553 return docstring 554 lines = docstring.splitlines(True) # keep line endings. 555 indents = [len(line) - len(line.lstrip()) for line in lines] 556 for k in range(len(lines) - 1): 557 if indents[k] > 0 or indents[k + 1]: 558 continue # only if no identation. 559 a, b = lines[k].strip(), lines[k + 1].strip() 560 if not (a and b): 561 continue # empty lines 562 if not b[0].isalpha(): 563 continue # second line must start with letter. 564 # join both lines. 565 lines[k] = lines[k].rstrip() + ' ' 566 lines[k + 1] = lines[k + 1].lstrip() 567 return ''.join(lines)
568 569 570 _ACTION_NAME_PREFIX = "Add-ons\\"
571 572 573 -def _unregisterActions():
574 store = empro.gui.actionStore() 575 for name in store.actionNames(): 576 if name.startswith(_ACTION_NAME_PREFIX): 577 store.unregisterAction(name)
578
579 580 -def _registerAction(item, prefix=None):
581 prefix = prefix or _ACTION_NAME_PREFIX 582 if isinstance(item, empro.gui.Menu): 583 for action in item.actions(): 584 _registerAction(action, "%s%s\\" % (prefix, item.title)) 585 return 586 store = empro.gui.actionStore() 587 name = prefix + item.text 588 suffix = 1 589 while store.action(name): 590 suffix += 1 591 name = "%s%s_%d" % (prefix, item.text, suffix) 592 store.registerAction(name, item)
593
594 595 -def _exampleNotification(path):
596 from empro import gui 597 preference = "%s/Notified" % _PREFERENCE_GROUP 598 name = os.path.splitext(os.path.basename(path))[0] 599 url = _ADDON_DOWNLOAD_URL 600 try: 601 notifieds = list(empro.core.ApplicationPreferences.getPreference(preference, None) or []) 602 except TypeError: 603 print "oops" 604 traceback.print_exc() 605 notifieds = [] 606 if name in notifieds: 607 return 608 gui.MessageBox.information("Example Add-on", '<p>%(name)s.py is an example add-on and provided as a demonstration only.</p>' 609 '<p>More add-ons and updates can be found on the <a href="%(url)s">Knowledge Center</a>.</p>' 610 '<p>See the <a href="http://edadocs.software.keysight.com/display/empro2012/EMPro+Add-ons">EMPro Documentation</a> for more info.</p>' % vars(), gui.Ok, gui.Ok) 611 notifieds.append(name) 612 empro.core.ApplicationPreferences.setPreference(preference, notifieds)
613