Source code for puzzlepiece.puzzle

from . import parse, threads

from pyqtgraph.Qt import QtWidgets, QtCore, QtGui
import ctypes
import os
import sys
import traceback


[docs] class Puzzle(QtWidgets.QWidget): """ A container for :class:`puzzlepiece.piece.Piece` objects, meant to be the main QWidget (window) of an automation application. It keeps track of the :class:`~puzzlepiece.piece.Piece` objects it contains and lets them communicate. A simple set up will look like this:: import puzzlepiece as pzp from puzzlepiece.pieces import random_number # Create a Qt app that will run our GUI, and the Puzzle app = pzp.QApp() puzzle = pzp.Puzzle(name="Basic example") # Add Pieces to the Puzzle puzzle.add_piece("random", random_number.Piece, row=0, column=0) # Show the Puzzle window and execute the Qt application puzzle.show() app.exec() The Qt app creation and call to ``exec`` can be skipped when running in IPython / Jupyter, but the ``%gui qt`` magic has to be used first to enable the GUI integration. When adding multiple :class:`~puzzlepiece.piece.Piece` s, the Puzzle can be used as a context manager, ensuring that any loaded APIs will be correctly unloaded in case any of the Pieces raises an exception during :func:`~puzzlepiece.piece.Piece.setup`:: with Puzzle(debug=False) as puzzle: # Any exceptions raised in this setup context will cause the Puzzle to shut down # gracefully, calling handle_close() on the Pieces added so far puzzle.add_piece("laser", laser.Piece, row=0, column=0) puzzle.add_piece("stage", stage.Piece, row=1, column=0) puzzle.show() # Note that the Puzzle object can still be used outside of the setup context puzzle["laser:power].set_value(10) :param app: A QtApp created to contain this QWidget. :param name: A name for the window. :param debug: Sets the Puzzle.debug property, if True the app should launch in debug mode and Pieces shouldn't communicate with hardware. :type debug: bool :param bottom_buttons: Whether the bottom buttons of the Puzzle (Tree, Export, STOP) should be shown. :type bottom_buttons: bool :param style: A Qt style to apply to the QApplication. puzzlepiece defaults to Fusion for cross-platform consistency, and adds some tweaks to make it look better. Set to None to maintain system-specific styling. """ def __init__( self, app=None, name="Puzzle", debug=True, bottom_buttons=True, style="Fusion", *args, **kwargs, ): super().__init__(*args, **kwargs) # Mark the Puzzle for deletion once it is closed self.setAttribute(QtCore.Qt.WidgetAttribute.WA_DeleteOnClose, True) # Pieces can handle the debug flag as they wish self._debug = debug self.app = app or QtWidgets.QApplication.instance() self.setWindowTitle(f"{name} (debug mode)" if self.debug else name) self._pieces = PieceDict() self._globals = Globals() # toplevel is used to send keypresses down the QWidget tree, # instead of up (which is how they normally propagate). # The list stores all the direct children of this QWidget self._toplevel = [] self._threadpool = QtCore.QThreadPool() # Set up styling # # Set window icon # On windows we have to tell the system that we are an application, otherwise the # default Python icon will appear - https://stackoverflow.com/a/1552105 if hasattr(ctypes, "windll"): myappid = "jdranczewski.github.io.puzzlepiece" ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid) # Make a QIcon and set it # We schedule this for when control returns to the main eventloop, otherwise # Windows sometimes fails to set the icon in the taskbar dirname = os.path.dirname(__file__) def set_icon(): self.setWindowIcon(QtGui.QIcon(os.path.join(dirname, "icon.png"))) del self._set_icon_later self._set_icon_later = threads.CallLater(set_icon) self._set_icon_later() # # Set the application style, and make some tweaks to it if its the # # puzzlepiece default (Fusion) self._stylesheet = "Popup {border:0;}" if style and style.lower() in [ key.lower() for key in QtWidgets.QStyleFactory.keys() ]: # Set the QApplication style if not already set. # The case on Fusion/fusion is not consistent, so we lower() throughout if ( ( hasattr(self.app.style(), "name") and style.lower() != self.app.style().name().lower() ) # name() was only introduced in Qt 6.1, use className in other versions or ( style.lower() != self.app.style() .metaObject() .className() .lower()[1 : -len("style")] ) ): self.app.setStyle(style) # Adjustments specific to the Fusion style if style.lower() == "fusion": # Stylesheet to make the Piece and group titles more clear self._stylesheet += """ Piece { font-weight: bold; } .QGroupBox { font-weight: bold; font-style: italic; } QGroupBox::title { left: 1ex; bottom: -0.5ex; } Folder { font-weight: bold; } """ palette = self.app.palette() if palette.color(palette.ColorRole.Window).lightness() < 150: # Dark mode! Add a bit to the stylesheet to make group boxes stand out more # (Fusion doesn't make them distinct enough by default) print("Dark mode!") self._stylesheet += """ Puzzle > Piece, Piece > QGroupBox, Grid > Piece { background-color: rgba(255, 255, 255, 15); } """ self.setStyleSheet(self._stylesheet) self.wrapper_layout = QtWidgets.QGridLayout() self.setLayout(self.wrapper_layout) self.layout = QtWidgets.QGridLayout() self.wrapper_layout.addLayout(self.layout, 0, 0) if bottom_buttons: self.wrapper_layout.addLayout(self._button_layout(), 1, 0) try: # If this doesn't raise a NameError, we're in IPython shell = get_ipython() # _orig_sys_module_state stores the original IPKernelApp excepthook, # irrespective of possible modifications in other cells self._old_excepthook = shell._orig_sys_module_state["excepthook"] # The following hack allows us to handle exceptions through the Puzzle in IPython. # Normally when a cell is executed in an IPython InteractiveShell, # sys.excepthook is overwritten with shell.excepthook, and then restored # to sys.excepthook after the cell run finishes. Any changes we make to # sys.excepthook in here directly will thus be overwritten as soon as the # cell that defines the Puzzle finishes running. # Instead, we schedule set_excepthook on a QTimer, meaning that it will # execute in the Qt loop rather than in a cell, so it can modify # sys.excepthook without risk of the changes being immediately overwritten, # For bonus points, we could set _old_excepthook to shell.excepthook, # which would result in all tracebacks appearing in the Notebook rather # than the console, but I think that is not desireable. def set_excepthook(): # Make sure we're out of the cell execution context if ( set_excepthook.counter < 100 and sys.excepthook is not self._old_excepthook ): # if not, we wait a little bit more set_excepthook.counter += 1 QtCore.QTimer.singleShot(500, set_excepthook) else: sys.excepthook = self._excepthook set_excepthook.counter = 0 QtCore.QTimer.singleShot(0, set_excepthook) except NameError: # In normal Python (not IPython) this is comparatively easy. # We use the original system hook here instead of sys.excepthook # to avoid unexpected behaviour if multiple things try to override # the hook in various ways. # If you need to implement custom exception handling, please assign # a value to your Puzzle's ``custom_excepthook`` method. self._old_excepthook = sys.__excepthook__ sys.excepthook = self._excepthook @property def pieces(self): """ A :class:`~puzzlepiece.puzzle.PieceDict`, effectively a dictionary of :class:`~puzzlepiece.piece.Piece` objects. Can be used to access Pieces from within other Pieces. You can also directly index the Puzzle object with the :class:`~puzzlepiece.piece.Piece` name, or even with a :class:`~puzzlepiece.piece.Piece` and a :class:`~puzzlepiece.param.BaseParam`:: # These two are equivalent puzzle.pieces["piece_name"] puzzle["piece_name"] # These three are equivalent puzzle.pieces["piece_name"].params["piece_name"] puzzle["piece_name"]["param_name"] puzzle["piece_name:param_name"] The valid keys for indexing a Puzzle object are available when autocompleting the key in IPython. """ return self._pieces @property def globals(self): """ A :class:`puzzlepiece.puzzle.Globals` object, effectively a dictionary, can be used for API modules that need to be shared by multiple Pieces. See :func:`puzzlepiece.puzzle.Globals.require` and :func:`puzzlepiece.puzzle.Globals.release` for advanced use. """ return self._globals @property def debug(self): """ A `bool` flag set on Puzzle creation. Pieces should act in debug mode if `True`. """ return self._debug # Adding elements
[docs] def add_piece( self, name, piece, row, column, rowspan=1, colspan=1, param_defaults=None ): """ Adds a :class:`~puzzlepiece.piece.Piece` to the grid layout, and registers it with the Puzzle. :param name: Identifying string for the Piece. :param piece: A :class:`~puzzlepiece.piece.Piece` object or a class defining one (which will be automatically instantiated). :param row: Row index for the grid layout. :param column: Column index for the grid layout. :param rowspan: Height in rows. :param colspan: Width in columns. :param param_defaults: An optional dictionary of default param values. These will be set without calling the corresponding param setters or :attr:`~puzzlepiece.param.BaseParam.changed` signals. :rtype: puzzlepiece.piece.Piece """ if isinstance(piece, type): piece = piece(self) self.register_piece(name, piece) if param_defaults: piece._set_param_defaults(param_defaults) self.layout.addWidget(piece, row, column, rowspan, colspan) self._toplevel.append(piece) return piece
[docs] def replace_piece(self, name, new_piece): """ Replace a named :class:`~puzzlepiece.piece.Piece` with a new one. Can be combined with ``importlib.reload`` to do live development on Pieces. This method is **experimental** and can sometimes fail. It's useful for development, but shouldn't really be used in production applications. :param name: Name of the Piece to be replaced. :param piece: A :class:`~puzzlepiece.piece.Piece` object or a class defining one (which will be automatically instantiated). """ old_piece = self.pieces[name] if isinstance(new_piece, type): new_piece = new_piece(self) if old_piece in self._toplevel: self.layout.replaceWidget( old_piece, new_piece, options=QtCore.Qt.FindChildOption.FindDirectChildrenOnly, ) new_piece.setTitle(name) self._toplevel.remove(old_piece) self._toplevel.append(new_piece) else: for widget in self._toplevel: if isinstance(widget, Folder): widget._replace_piece(name, old_piece, new_piece) self._pieces._replace_item(name, new_piece) if not self.debug: old_piece.handle_close(None) # old_piece.deleteLater() old_piece.setAttribute(QtCore.Qt.WidgetAttribute.WA_DeleteOnClose, True) old_piece.close()
[docs] def add_folder(self, row, column, rowspan=1, colspan=1): """ Adds a tabbed :class:`~puzzlepiece.puzzle.Folder` to the grid layout, and returns it. :param row: Row index for the grid layout. :param column: Column index for the grid layout. :param rowspan: Height in rows. :param column: Width in columns. :rtype: puzzlepiece.puzzle.Folder """ folder = Folder(self) self.layout.addWidget(folder, row, column, rowspan, colspan) self._toplevel.append(folder) return folder
[docs] def register_piece(self, name, piece): """ Registers a :class:`~puzzlepiece.piece.Piece` object with the Puzzle. This is done by default when a :class:`~puzzlepiece.piece.Piece` is added with :func:`~puzzlepiece.puzzle.Puzzle.add_piece`, :func:`puzzlepiece.puzzle.Folder.add_piece`, or :func:`puzzlepiece.puzzle.Grid.add_piece`, so this method should rarely be called manually. """ self.pieces[name] = piece piece._name = name piece.setTitle(name)
# Other methods
[docs] def process_events(self): """ Forces the QApplication to process events that happened while a callback was executing. Can for example update plots while a long process is running, or run any keyboard shortcuts pressed while proecessing. """ self.app.processEvents()
_shutdown_threads = QtCore.Signal()
[docs] def run_worker(self, worker): """ Add a Worker to the Puzzle's Threadpool and runs it. See :class:`puzzlepiece.threads` for more details on how to set up a Worker. """ if hasattr(worker, "stop"): # This signal is emitted when the application is shutting down, # so we're telling the LiveWorker to stop self._shutdown_threads.connect(worker.stop) self._threadpool.start(worker)
def _excepthook(self, exctype, value, traceback): self._old_excepthook(exctype, value, traceback) # Stop any threads that may be running self._shutdown_threads.emit() # Only do custom exception handling in the main thread, otherwise the messagebox # or other such things are likely to break things. if QtCore.QThread.currentThread() == self.app.thread(): self.custom_excepthook(exctype, value, traceback) box = QtWidgets.QMessageBox() box.setText(str(value) + "\n\nCheck console for details.") box.exec()
[docs] def custom_excepthook(self, exctype, value, traceback): """ Override or replace this method to call a custom handler whenever an exception is raised. This will run after the defatult exception handler (``sys.__excepthook__``), but before a GUI alert is displayed. """ pass
# Convenience methods def _docs(self): dialog = QtWidgets.QDialog(self) layout = QtWidgets.QVBoxLayout() tree = QtWidgets.QTreeWidget() tree.setHeaderLabels(("pieces", "get?", "set?")) def copy_item(item): if hasattr(item, "puzzlepiece_descriptor"): self.app.clipboard().setText(item.puzzlepiece_descriptor) tree.itemDoubleClicked.connect(copy_item) for piece_name in self.pieces: piece_item = QtWidgets.QTreeWidgetItem(tree, (piece_name,)) # First, params tree_item = QtWidgets.QTreeWidgetItem(piece_item, ("params",)) for param_name in self.pieces[piece_name].params: param = self.pieces[piece_name].params[param_name] G = "⟳" if param._getter is not None else "" S = "✓" if param._setter is not None else "" param_item = QtWidgets.QTreeWidgetItem(tree_item, (param_name, G, S)) param_item.puzzlepiece_descriptor = "{}:{}".format( piece_name, param_name ) # Then, actions tree_item = QtWidgets.QTreeWidgetItem(piece_item, ("actions",)) for action_name in self.pieces[piece_name].actions: action = self.pieces[piece_name].actions[action_name] action_item = QtWidgets.QTreeWidgetItem(tree_item, (action_name,)) action_item.puzzlepiece_descriptor = "{}:{}".format( piece_name, action_name ) button = QtWidgets.QToolButton() icon = self.style().standardIcon( QtWidgets.QStyle.StandardPixmap.SP_MediaPlay ) button.setIcon(icon) button.clicked.connect(lambda x=False, action=action: action()) tree.setItemWidget(action_item, 1, button) for i in range(0, 3): tree.header().setSectionResizeMode( i, QtWidgets.QHeaderView.ResizeMode.ResizeToContents ) label = QtWidgets.QLabel() label.setText( "Double-click on any row to copy the param/action identifier for use in scripts." ) label.setWordWrap(True) layout.addWidget(tree) layout.addWidget(label) dialog.setLayout(layout) dialog.show() dialog.raise_() dialog.activateWindow() def _export_setup(self): dialog = QtWidgets.QDialog(self) layout = QtWidgets.QVBoxLayout() label = QtWidgets.QLabel() label.setText( "The script below sets all currently params that don't have setters or getters to the current values." ) label.setWordWrap(True) layout.addWidget(label) text = "" for piece_name in self.pieces: keys = self.pieces[piece_name].params for key in keys: param = self.pieces[piece_name].params[key] if param.visible and param._setter is None and param._getter is None: text += "set:{}:{}:{}\n".format(piece_name, key, param.get_value()) text_box = QtWidgets.QPlainTextEdit() text_box.setPlainText(text) layout.addWidget(text_box) button = QtWidgets.QPushButton("Save") def __save_export(): fname = str(QtWidgets.QFileDialog.getSaveFileName(self, "Save file...")[0]) if len(fname): with open(fname, "w") as f: f.write(text_box.toPlainText()) button.clicked.connect(__save_export) layout.addWidget(button) dialog.setLayout(layout) dialog.show() dialog.raise_() dialog.activateWindow() def _call_stop(self): for piece_name in self.pieces: self.pieces[piece_name].call_stop() self._shutdown_threads.emit() def _button_layout(self): layout = QtWidgets.QHBoxLayout() for function, icon, text in zip( (self._docs, self._export_setup, self._call_stop), ( QtWidgets.QStyle.StandardPixmap.SP_MessageBoxInformation, QtWidgets.QStyle.StandardPixmap.SP_DialogSaveButton, QtWidgets.QStyle.StandardPixmap.SP_BrowserStop, ), ("Tree (F1)", "Export (F2)", "STOP (F3)"), ): button = QtWidgets.QPushButton(text) icon = self.style().standardIcon(icon) button.setIcon(icon) button.clicked.connect(lambda x=False, action=function: action()) layout.addWidget(button) return layout def __getitem__(self, name): return self.pieces[name] def _ipython_key_completions_(self): values = list(self.pieces.keys()) for piece in self.pieces.keys(): values.extend([f"{piece}:{param}" for param in self.pieces[piece].params]) return values def __enter__(self): return self def __exit__(self, t, v, tb): if t is not None: self._handle_close() return None
[docs] def run(self, text): """ Execute script commands for this Puzzle as described in :func:`puzzlepiece.parse.run`. """ parse.run(text, self)
[docs] def get_values(self, text): """ Get the values from multiple params as a list. :param text: A string of comma-separated param strings as described in :func:`puzzlepiece.parse.parse_params`. :rtype: list """ return [param.get_value() for param in parse.parse_params(text, self)]
[docs] def record_values(self, text, dictionary=None): """ Get the values from multiple params and record them in a dictionary. Useful for storing metadata about a measurement. :param text: A string of comma-separated param strings as described in :func:`puzzlepiece.parse.parse_params`. :param dictionary: If provided, this function will write the param names and values to this dictionary. Otherwise, a new one is created and returned. :rtype: dict """ params = parse.parse_params(text, self) names = text.split(", ") if dictionary is None: dictionary = {} for name, param in zip(names, params): dictionary[name] = param.get_value() return dictionary
# Qt overrides def keyPressEvent(self, event): """ Pass down keypress events to child Pieces and Folders. Overwrites a QT method. :meta private: """ if event.key() == QtCore.Qt.Key.Key_F1: self._docs() elif event.key() == QtCore.Qt.Key.Key_F2: self._export_setup() elif event.key() == QtCore.Qt.Key.Key_F3: self._call_stop() for widget in self._toplevel: widget.handle_shortcut(event) _close_popups = QtCore.Signal() def _handle_close(self, event=None): """ Tell the Pieces the window is closing, so they can for example disconnect hardware. """ # self._shutdown_threads.emit() self._call_stop() self._close_popups.emit() if not self.debug: for piece_name in self.pieces: # We need to make sure we call all the handle_close methods, as # well as the excepthook swap at the end, so we print the tracebacks # instead of re-raising try: self.pieces[piece_name].handle_close(event) except Exception: print("Exception while calling handle_close:") print(traceback.format_exc()) # Reinstate the original excepthook sys.excepthook = self._old_excepthook def closeEvent(self, event): """ Overwrites a QT method to call ``_handle_close``. :meta private: """ self._handle_close(event) super().closeEvent(event)
[docs] def QApp(args=None): """A QApplication has to be constructed before any Qt objects (including the Puzzle and the Pieces), so this is a convenient shortcut to instance the QApplication class (see https://doc.qt.io/qt-6/qapplication.html). Only one QApplication can exist at a time, so if there is already an instance, this function returns it instead of creating a new one. :param args: list of strings to pass as arguments when creating the QApplication """ instance = QtWidgets.QApplication.instance() if instance and args: print( "puzzlepiece.QApp WARNING: A QApplication already exists, ignoring provided arguments." ) args = args or [] return instance or QtWidgets.QApplication(args)
[docs] class Folder(QtWidgets.QTabWidget): """ A tabbed group of :class:`~puzzlepiece.puzzle.Piece` or :class:`~puzzlepiece.puzzle.Grid` objects within the :class:`~puzzlepiece.puzzle.Puzzle`. Best created with :func:`puzzlepiece.puzzle.Puzzle.add_folder`. """ def __init__(self, puzzle, *args, **kwargs): super().__init__(*args, **kwargs) self.puzzle = puzzle self.pieces = []
[docs] def add_piece(self, name, piece, param_defaults=None): """ Adds a :class:`~puzzlepiece.piece.Piece` as a tab to this Folder, and registers it with the parent :class:`~puzzlepiece.puzzle.Puzzle`. :param name: Identifying string for the Piece. :param piece: A :class:`~puzzlepiece.piece.Piece` object or a class defining one (which will be automatically instantiated). :param param_defaults: An optional dictionary of default param values. These will be set without calling the corresponding param setters or :attr:`~puzzlepiece.param.BaseParam.changed` signals. :rtype: puzzlepiece.piece.Piece """ if isinstance(piece, type): piece = piece(self.puzzle) self.puzzle.register_piece(name, piece) if param_defaults: piece._set_param_defaults(param_defaults) self.addTab(piece, name) self.pieces.append(piece) piece.folder = self # No title or border displayed when Piece in Folder piece.setTitle(None) piece.setStyleSheet("Piece {border:0;}") # Remove most of the border if the stylesheet fails piece.setFlat(True) return piece
[docs] def add_grid(self, name): """ Adds a :class:`~puzzlepiece.puzzle.Grid` as a tab to this Folder. :param name: Identifying string for the :class:`~puzzlepiece.puzzle.Grid`. :rtype: puzzlepiece.puzzle.Grid """ grid = Grid(self.puzzle) self.addTab(grid, name) self.pieces.append(grid) grid.folder = self return grid
def handle_shortcut(self, event): """ Pass down keypress events only to the **active** child :class:`~puzzlepiece.piece.Piece` or :class:`~puzzlepiece.puzzle.Grid`. :meta private: """ self.currentWidget().handle_shortcut(event) def _replace_piece(self, name, old_piece, new_piece): if old_piece in self.pieces: index = self.indexOf(old_piece) self.insertTab(index, new_piece, name) new_piece.folder = self # No title or border displayed when Piece in Folder new_piece.setTitle(None) new_piece.setStyleSheet("Piece {border:0;}") new_piece.setFlat(True) self.pieces.remove(old_piece) self.pieces.append(new_piece) else: for widget in self.pieces: if isinstance(widget, Grid): widget._replace_piece(name, old_piece, new_piece)
[docs] class Grid(QtWidgets.QWidget): """ A grid layout for :class:`~puzzlepiece.piece.Piece` objects. For when you need multiple Pieces within a single :class:`~puzzlepiece.puzzle.Folder` tab. Best created with :func:`puzzlepiece.puzzle.Puzzle.add_folder`. """ def __init__(self, puzzle, *args, **kwargs): super().__init__(*args, **kwargs) self.puzzle = puzzle self.pieces = [] self.layout = QtWidgets.QGridLayout() self.setLayout(self.layout)
[docs] def add_piece( self, name, piece, row, column, rowspan=1, colspan=1, param_defaults=None ): """ Adds a :class:`~puzzlepiece.piece.Piece` to the grid layout, and registers it with the parent :class:`~puzzlepiece.puzzle.Puzzle`. :param name: Identifying string for the Piece. :param piece: A :class:`~puzzlepiece.piece.Piece` object or a class defining one (which will be automatically instantiated). :param row: Row index for the grid layout. :param column: Column index for the grid layout. :param rowspan: Height in rows. :param colspan: Width in columns. :param param_defaults: An optional dictionary of default param values. These will be set without calling the corresponding param setters or :attr:`~puzzlepiece.param.BaseParam.changed` signals. :rtype: puzzlepiece.piece.Piece """ if isinstance(piece, type): piece = piece(self.puzzle) self.puzzle.register_piece(name, piece) if param_defaults: piece._set_param_defaults(param_defaults) self.layout.addWidget(piece, row, column, rowspan, colspan) self.pieces.append(piece) piece.folder = self return piece
def _replace_piece(self, name, old_piece, new_piece): if old_piece in self.pieces: self.layout.replaceWidget(old_piece, new_piece) new_piece.setTitle(name) new_piece.folder = self self.pieces.remove(old_piece) self.pieces.append(new_piece) def handle_shortcut(self, event): """ Pass down keypress events to child Pieces. :meta private: """ for widget in self.pieces: widget.handle_shortcut(event) def setCurrentWidget(self, piece): """ Passes the `setCurrentWidget` to this Grid's Folder, allowing Pieces within this Grid to correctly call :func:`puzzlepiece.piece.Piece.elevate`. :meta private: """ if self.folder is not None: self.folder.setCurrentWidget(self)
[docs] class PieceDict: """ A dictionary wrapper that enforces single-use of keys, and raises a more useful error when a Piece tries to use another Piece that hasn't been registered. It also allows indexing params directly by using this key format: ``[piece_name]:[param_name]``. """ def __init__(self): self._dict = {} def __setitem__(self, key, value): if key in self._dict: raise KeyError("A Piece with id '{}' already exists".format(key)) self._dict[key] = value def __iter__(self): for key in self._dict: yield key def __getitem__(self, key): if key not in self._dict: try: piece, param = key.split(":") return self._dict[piece][param] except ValueError: # key is not in the piece:param format pass raise KeyError( "A Piece with id '{}' is required, but doesn't exist".format(key) ) return self._dict[key] def _replace_item(self, key, value): self._dict[key] = value def __contains__(self, item): return item in self._dict
[docs] def keys(self): return self._dict.keys()
def __repr__(self): return "PieceDict({})".format(", ".join(self._dict.keys()))
[docs] class Globals(QtCore.QObject): """ A dictionary wrapper used for :attr:`puzzlepiece.puzzle.Puzzle.globals`. It behaves like a dictionary, allowing :class:`puzzlepiece.piece.Piece` objects to share device APIs with each other. Additionally, :func:`~puzzlepiece.puzzle.Globals.require` and :func:`~puzzlepiece.puzzle.Globals.release` can be used to keep track of the Pieces using a given variable, so that the API can be loaded once and then unloaded once all the Pieces are done with it. """ def __init__(self): self._dict = {} self._counts = {} super().__init__()
[docs] def require(self, name): """ Register that a Piece is using the variable with a given name. This will increase an internal counter to indicate the Piece having a hold on the variable. Returns `False` if this is the first time a variable is being registered (and thus setup is needed) or `True` if the variable has been registered already. For example, this can be used within :func:`~puzzlepiece.piece.Piece.setup`:: def setup(self): if not self.puzzle.globals.require('sdk'): # Load the SDK if not done already by a different Piece self.puzzle.globals['sdk'] = self.load_sdk() :param name: a dictionary key for the required variable :rtype: bool """ if name not in self._dict: self._dict[name] = None self._counts[name] = 1 return False else: self._counts[name] += 1 return True
[docs] def release(self, name): """ Indicate that a Piece is done using the variable with a given name. This will decrease an internal counter to indicate the Piece is releasing its hold on the variable. Returns `False` if the counter is non-zero (so different Pieces are still using this variable) or `True` if all Pieces are done with the variable (in that case the SDK can be safely shut down for example). For example, this can be used within :func:`~puzzlepiece.piece.Piece.handle_close`:: def handle_close(self): if self.puzzle.globals.release('sdk'): # Unload the SDK if all Pieces are done with it self.puzzle.globals['sdk'].stop() :param name: a dictionary key for the variable being released :rtype: bool """ if name not in self._dict: raise KeyError(f"No global variable with id '{name}' to release") if name not in self._counts: raise KeyError( f"Cannot release '{name}' since it hasn't been registered with 'require'" ) self._counts[name] -= 1 return self._counts[name] < 1
def __setitem__(self, key, value): self._dict[key] = value def __getitem__(self, key): if key not in self._dict: raise KeyError("No global variable with id '{}'".format(key)) return self._dict[key] #: A Qt signal called when a Globals key is deleted. The key is passed as the argument. #: You can use this when multiple Pieces share the same API instance - the other Pieces #: can connect to this Signal and handle the API being deleted. deleted = QtCore.Signal(object) def __delitem__(self, key): del self._dict[key] self.deleted.emit(key) if key in self._counts: del self._counts[key] def __contains__(self, item): return item in self._dict
[docs] def keys(self): return self._dict.keys()
def __repr__(self): return "Globals({})".format(", ".join(self._dict.keys()))
[docs] class PretendPuzzle: """ A placeholder object used if no :class:`~puzzlepiece.puzzle.Puzzle` is provided when creating a :class:`puzzlepiece.puzzle.Piece`. Its `debug` attribute is always True. """ debug = True
[docs] def process_events(self): """ Like :func:`puzzlepiece.puzzle.Puzzle.process_events()`. """ QtWidgets.QApplication.instance().processEvents()