Skip to content

Mu API Reference

This API reference is automatically generated from the docstrings found within the source code. It's meant as an easy to use and easy to share window into the code base.

Take a look around! The code is simple and short.

mu.app

The Mu application is created and configured in this module.

mu.app

Mu - a "micro" Python editor for beginner programmers.

Copyright (c) 2015-2017 Nicholas H.Tollervey and others (see the AUTHORS file).

Based upon work done for Puppy IDE by Dan Pope, Nicholas Tollervey and Damien George.

This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/.

AnimatedSplash

Bases: QSplashScreen

An animated splash screen for gifs. Includes a text area for logging output.

Source code in mu/app.py
class AnimatedSplash(QSplashScreen):
    """
    An animated splash screen for gifs. Includes a text area for logging
    output.
    """

    def __init__(self, animation, parent=None):
        """
        Ensure signals are connected and start the animation.
        """
        self.log_lines = 4
        # To hold only number of log_lines of logs to display.
        self.log = []
        self.animation = animation
        self.animation.frameChanged.connect(self.set_frame)
        # Always on top.
        super().__init__(self.animation.currentPixmap())
        # Disable clicks.
        self.setEnabled(False)
        self.animation.start()

    def set_frame(self):
        """
        Update the splash screen with the next frame of the animation.
        """
        pixmap = self.animation.currentPixmap()
        self.setPixmap(pixmap)
        self.setMask(pixmap.mask())

    def draw_log(self, text):
        """
        Draw the log entries onto the splash screen. Will only display the last
        self.log_lines number of log entries. The logs will be displayed at the
        bottom of the splash screen, justified left.
        """
        self.log.append(text)
        self.log = self.log[-self.log_lines :]
        if self.log:
            self.draw_text("\n".join(self.log))

    def draw_text(self, text):
        """
        Draw text into splash screen.
        """
        if text:
            self.showMessage(
                text, Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignLeft
            )

    def failed(self, text):
        """
        Something has gone wrong during start-up, so signal this, display a
        helpful message along with instructions for what to do.
        """
        self.animation.stop()
        pixmap = load_pixmap("splash_fail.png")
        self.setPixmap(pixmap)
        lines = text.split("\n")
        lines.append(
            "This screen will close in a few seconds. "
            "Then a crash report tool will open in your browser."
        )
        lines = lines[-12:]
        self.draw_text("\n".join(lines))

__init__(animation, parent=None)

Ensure signals are connected and start the animation.

Source code in mu/app.py
def __init__(self, animation, parent=None):
    """
    Ensure signals are connected and start the animation.
    """
    self.log_lines = 4
    # To hold only number of log_lines of logs to display.
    self.log = []
    self.animation = animation
    self.animation.frameChanged.connect(self.set_frame)
    # Always on top.
    super().__init__(self.animation.currentPixmap())
    # Disable clicks.
    self.setEnabled(False)
    self.animation.start()

draw_log(text)

Draw the log entries onto the splash screen. Will only display the last self.log_lines number of log entries. The logs will be displayed at the bottom of the splash screen, justified left.

Source code in mu/app.py
def draw_log(self, text):
    """
    Draw the log entries onto the splash screen. Will only display the last
    self.log_lines number of log entries. The logs will be displayed at the
    bottom of the splash screen, justified left.
    """
    self.log.append(text)
    self.log = self.log[-self.log_lines :]
    if self.log:
        self.draw_text("\n".join(self.log))

draw_text(text)

Draw text into splash screen.

Source code in mu/app.py
def draw_text(self, text):
    """
    Draw text into splash screen.
    """
    if text:
        self.showMessage(
            text, Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignLeft
        )

failed(text)

Something has gone wrong during start-up, so signal this, display a helpful message along with instructions for what to do.

Source code in mu/app.py
def failed(self, text):
    """
    Something has gone wrong during start-up, so signal this, display a
    helpful message along with instructions for what to do.
    """
    self.animation.stop()
    pixmap = load_pixmap("splash_fail.png")
    self.setPixmap(pixmap)
    lines = text.split("\n")
    lines.append(
        "This screen will close in a few seconds. "
        "Then a crash report tool will open in your browser."
    )
    lines = lines[-12:]
    self.draw_text("\n".join(lines))

set_frame()

Update the splash screen with the next frame of the animation.

Source code in mu/app.py
def set_frame(self):
    """
    Update the splash screen with the next frame of the animation.
    """
    pixmap = self.animation.currentPixmap()
    self.setPixmap(pixmap)
    self.setMask(pixmap.mask())

SharedMemoryMutex

Bases: object

Simple wrapper around the QSharedMemory object, adding a context handler which uses the built in Semaphore as a locking mechanism and raises an error if the shared memory object is already in use

TODO: The *nix implementation doesn't release the shared memory if a process attached to it crashes. There is code to attempt to detect this but it doesn't seem worth implementing for the moment: we're only talking about a page at most.

Source code in mu/app.py
class SharedMemoryMutex(object):
    """Simple wrapper around the QSharedMemory object, adding a context
    handler which uses the built in Semaphore as a locking mechanism
    and raises an error if the shared memory object is already in use

    TODO: The *nix implementation doesn't release the shared memory if a
    process attached to it crashes. There is code to attempt to detect this
    but it doesn't seem worth implementing for the moment: we're only talking
    about a page at most.
    """

    NAME = "mu-tex"

    def __init__(self):
        sharedAppName = self.NAME
        if "MU_TEST_SUPPORT_RANDOM_APP_NAME_EXT" in os.environ:
            sharedAppName += os.environ["MU_TEST_SUPPORT_RANDOM_APP_NAME_EXT"]
        self._shared_memory = QSharedMemory(sharedAppName)

    def __enter__(self):
        self._shared_memory.lock()
        return self

    def __exit__(self, *args, **kwargs):
        self._shared_memory.unlock()

    def acquire(self):
        #
        # The attach-detach dance is a shim from
        # https://stackoverflow.com/questions/42549904/qsharedmemory-is-not-getting-deleted-on-application-crash
        # If the existing shared memory is not held by any active application
        # (eg because an appimage has hard-crashed) then it will be released
        # If the memory is held by an active application it will have no effect
        #
        self._shared_memory.attach()
        self._shared_memory.detach()

        if self._shared_memory.attach():
            pid = struct.unpack("q", self._shared_memory.data()[:8])
            raise MutexError("MUTEX: Mu is already running with pid %d" % pid)
        else:
            self._shared_memory.create(8)
            self._shared_memory.data()[:8] = struct.pack("q", os.getpid())

    def release(self):
        self._shared_memory.detach()

StartupWorker

Bases: QObject

A worker class for running blocking tasks on a separate thread during application start-up.

The animated splash screen will be shown until this thread is finished.

Source code in mu/app.py
class StartupWorker(QObject):
    """
    A worker class for running blocking tasks on a separate thread during
    application start-up.

    The animated splash screen will be shown until this thread is finished.
    """

    finished = pyqtSignal()  # emitted when successfully finished.
    failed = pyqtSignal(str)  # emitted if finished with an error.
    display_text = pyqtSignal(str)  # emitted to update the splash text.

    def run(self):
        """
        Blocking and long running tasks for application startup should be
        called from here.
        """
        try:
            venv.ensure_and_create(self.display_text)
            self.finished.emit()  # Always called last.
        except Exception as ex:
            # Catch all exceptions just in case.
            # Report the failure, along with a summary to show the user.
            stack = traceback.extract_stack()[:-1]
            msg = "\n".join(traceback.format_list(stack))
            msg += "\n\n" + traceback.format_exc()
            self.failed.emit(msg)
            # Sleep a while in the thread so the user sees something is wrong.
            time.sleep(7)
            self.finished.emit()
            # Re-raise for crash handler to kick in.
            raise ex
        finally:
            # Always clean up the startup splash/venv logging handlers.
            if vlogger.handlers:
                handler = vlogger.handlers[0]
                vlogger.removeHandler(handler)

run()

Blocking and long running tasks for application startup should be called from here.

Source code in mu/app.py
def run(self):
    """
    Blocking and long running tasks for application startup should be
    called from here.
    """
    try:
        venv.ensure_and_create(self.display_text)
        self.finished.emit()  # Always called last.
    except Exception as ex:
        # Catch all exceptions just in case.
        # Report the failure, along with a summary to show the user.
        stack = traceback.extract_stack()[:-1]
        msg = "\n".join(traceback.format_list(stack))
        msg += "\n\n" + traceback.format_exc()
        self.failed.emit(msg)
        # Sleep a while in the thread so the user sees something is wrong.
        time.sleep(7)
        self.finished.emit()
        # Re-raise for crash handler to kick in.
        raise ex
    finally:
        # Always clean up the startup splash/venv logging handlers.
        if vlogger.handlers:
            handler = vlogger.handlers[0]
            vlogger.removeHandler(handler)

check_only_running_once()

If the application is already running log the error and exit

Source code in mu/app.py
def check_only_running_once():
    """If the application is already running log the error and exit"""
    try:
        with _shared_memory:
            _shared_memory.acquire()
    except MutexError as exc:
        [message] = exc.args
        logging.error(message)
        sys.exit(2)

crash_details(exc_type, exc_value, exc_traceback)

The text a user can copy into a bug report after a crash: versions, platform, where the log is, and the traceback.

Source code in mu/app.py
def crash_details(exc_type, exc_value, exc_traceback):
    """
    The text a user can copy into a bug report after a crash: versions,
    platform, where the log is, and the traceback.
    """
    return "\n".join(
        [
            "Nu {}".format(__version__),
            "Python {} on {}".format(platform.python_version(), platform.platform()),
            "Log file: {}".format(LOG_FILE),
            "",
            "".join(
                traceback.format_exception(exc_type, exc_value, exc_traceback)
            ).rstrip(),
        ]
    )

excepthook(*exc_args)

Log the exception, show the details to the user, and exit -- unless it came from a plugin, in which case it's reported to the user and Nu carries on.

Source code in mu/app.py
def excepthook(*exc_args):
    """
    Log the exception, show the details to the user, and exit -- unless it
    came from a plugin, in which case it's reported to the user and Nu
    carries on.
    """
    if plugin_errors.handle_uncaught(*exc_args):
        return
    logging.error("Unrecoverable error", exc_info=(exc_args))
    # Very important to release shared memory used to signal an app instance is running
    # as we are going to exit below
    _shared_memory.release()
    if exc_args[0] is not KeyboardInterrupt:
        try:
            show_crash_dialog(crash_details(*exc_args))
        except Exception as e:  # The Alamo of crash handling.
            logging.error("Failed to show the crash details", exc_info=e)
        sys.__excepthook__(*exc_args)
        sys.exit(1)
    else:  # It's harmless, don't sound the alarm.
        sys.exit(0)

is_linux_wayland()

Checks environmental variables to try to determine if Mu is running on wayland.

Source code in mu/app.py
def is_linux_wayland():
    """
    Checks environmental variables to try to determine if Mu is running on
    wayland.
    """
    if platform.system() == "Linux":
        for env_var in ("XDG_SESSION_TYPE", "WAYLAND_DISPLAY"):
            if "wayland" in os.environ.get(env_var, "").lower():
                return True
    return False

run()

Creates all the top-level assets for the application, sets things up and then runs the application. Specific tasks include:

  • set up logging
  • set up global exception handler
  • check that another instance of the app isn't already running (exit if so)
  • create an application object
  • create an editor window and status bar
  • display a splash screen while starting
  • close the splash screen after startup timer ends
Source code in mu/app.py
def run():
    """
    Creates all the top-level assets for the application, sets things up and
    then runs the application. Specific tasks include:

    - set up logging
    - set up global exception handler
    - check that another instance of the app isn't already running (exit if so)
    - create an application object
    - create an editor window and status bar
    - display a splash screen while starting
    - close the splash screen after startup timer ends
    """
    setup_logging()
    logging.info("\n\n-----------------\n\nStarting Mu {}".format(__version__))
    logging.info(platform.uname())
    logging.info("Process id: {}".format(os.getpid()))
    logging.info("Platform: {}".format(platform.platform()))
    logging.info("Python path: {}".format(sys.path))
    logging.info("Language code: {}".format(i18n.language_code))
    setup_exception_handler()
    check_only_running_once()

    # Remove the VIRTUAL_ENV env var: when set, prevents pgzero mode from working
    # if Mu is installed from source/PyPI into a virtual environment.
    os.environ.pop("VIRTUAL_ENV", None)

    #
    # Load settings from known locations and register them for
    # autosave
    #
    settings.init()

    # High-DPI scaling (retina/4k) and high-resolution pixmaps are always
    # enabled in Qt6, so the AA_EnableHighDpiScaling / AA_UseHighDpiPixmaps
    # attributes that were needed under Qt5 have been removed.

    # An issue in PyQt5 v5.13.2 to v5.15.1 made the application hang on
    # macOS 11 (Big Sur); setting this environment variable fixed it. It's a
    # no-op under Qt6 but left in place as a harmless safeguard.
    # See issue #1147 for more information
    os.environ["QT_MAC_WANTS_LAYER"] = "1"

    # In Wayland for AppImage to launch it needs QT_QPA_PLATFORM set
    # But only touch it if unset, useful for CI to configure it to "offscreen"
    if is_linux_wayland():
        if "QT_QPA_PLATFORM" not in os.environ:
            logging.info("Wayland detected, setting QT_QPA_PLATFORM=wayland")
            os.environ["QT_QPA_PLATFORM"] = "wayland"
        else:
            logging.info(
                "Wayland detected, QT_QPA_PLATFORM already set to: {}".format(
                    os.environ["QT_QPA_PLATFORM"]
                )
            )

    # The app object is the application running on your computer.
    app = QApplication(sys.argv)
    # By default PyQt uses the script name (run.py)
    app.setApplicationName("mu")
    # Set hint as to the .desktop files name: Briefcase's app id
    # (`bundle` + app name in pyproject.toml), so the Wayland app id matches
    # the installed org.kovir.mu.desktop (taskbar icon/grouping).
    app.setDesktopFileName("org.kovir.mu")
    app.setApplicationVersion(__version__)
    app.setAttribute(Qt.ApplicationAttribute.AA_DontShowIconsInMenus)

    def splash_context():
        """
        Function context (to ensure garbage collection) for displaying the
        splash screen.
        """
        # Display a friendly "splash" icon.
        splash = AnimatedSplash(load_movie("splash_screen"))
        splash.show()

        # Create a blocking thread upon which to run the StartupWorker and which
        # will process the events for animating the splash screen.
        initLoop = QEventLoop()
        thread = QThread()
        worker = StartupWorker()
        worker.moveToThread(thread)
        thread.started.connect(worker.run)
        worker.finished.connect(thread.quit)
        worker.finished.connect(worker.deleteLater)
        worker.display_text.connect(splash.draw_log)
        worker.failed.connect(splash.failed)
        # Stop the blocking event loop when the thread is finished.
        thread.finished.connect(initLoop.quit)
        thread.finished.connect(thread.deleteLater)
        thread.start()
        initLoop.exec()  # start processing the pending StartupWorker.
        splash.close()
        splash.deleteLater()

    splash_context()

    # Create the "window" we'll be looking at.
    editor_window = Window()

    @editor_window.load_theme.connect
    def load_theme(theme):
        if theme == "contrast":
            app.setStyleSheet(CONTRAST_STYLE)
        elif theme == "night":
            app.setStyleSheet(NIGHT_STYLE)
        else:
            app.setStyleSheet(DAY_STYLE)

    # Make sure all windows have the Mu icon as a fallback
    app.setWindowIcon(load_icon(editor_window.icon))
    # Create the "editor" that'll control the "window".
    editor = Editor(view=editor_window)
    editor.setup(setup_modes(editor, editor_window))
    # Setup the window.
    editor_window.closeEvent = editor.quit
    editor_window.setup(editor.debug_toggle_breakpoint, editor.theme)
    # Connect the various UI elements in the window to the editor.
    editor_window.connect_tab_rename(editor.rename_tab, "Ctrl+Shift+S")
    editor_window.connect_find_replace(editor.find_replace, "Ctrl+F")
    # Connect find again both forward and backward ('Shift+F3')
    find_again_handlers = (editor.find_again, editor.find_again_backward)
    editor_window.connect_find_again(find_again_handlers, "F3")
    editor_window.connect_toggle_comments(editor.toggle_comments, "Ctrl+K")
    editor.connect_to_status_bar(editor_window.status_bar)

    # Restore the previous session along with files passed by the os
    editor.restore_session(sys.argv[1:])

    # Save the exit code for sys.exit call below.
    exit_status = app.exec()
    # Clean up the shared memory used to signal an app instance is running
    _shared_memory.release()

    # Stop the program after the application finishes executing.
    sys.exit(exit_status)

setup_exception_handler()

Install global exception handler

Source code in mu/app.py
def setup_exception_handler():
    """
    Install global exception handler
    """
    sys.excepthook = excepthook

setup_logging()

Configure logging.

Source code in mu/app.py
def setup_logging():
    """
    Configure logging.
    """
    os.makedirs(LOG_DIR, exist_ok=True)

    # set logging format
    log_fmt = (
        "%(asctime)s - %(name)s:%(lineno)d(%(funcName)s) %(levelname)s: %(message)s"
    )
    formatter = logging.Formatter(log_fmt)

    # define log handlers such as for rotating log files
    handler = TimedRotatingFileHandler(
        LOG_FILE, when="midnight", backupCount=5, delay=0, encoding=ENCODING
    )
    handler.setFormatter(formatter)
    handler.setLevel(logging.DEBUG)

    # set up primary log
    log = logging.getLogger()
    log.setLevel(logging.DEBUG)
    log.addHandler(handler)

    # Only enable on-screen logging if the MU_LOG_TO_STDOUT env variable is set
    if "MU_LOG_TO_STDOUT" in os.environ:
        stdout_handler = logging.StreamHandler()
        stdout_handler.setFormatter(formatter)
        stdout_handler.setLevel(logging.DEBUG)
        log.addHandler(stdout_handler)

setup_modes(editor, view)

Create a dictionary of the available modes, keyed by short name.

The built-in modes are registered through a plugin Host, then every installed or dropped-in plugin that is switched on is loaded into it. A plugin that fails to load is logged and skipped.

Source code in mu/app.py
def setup_modes(editor, view):
    """
    Create a dictionary of the available modes, keyed by short name.

    The built-in modes are registered through a plugin Host, then every
    installed or dropped-in plugin that is switched on is loaded into it. A
    plugin that fails to load is logged and skipped.
    """
    host = Host(editor, view)
    for mode_class in (PythonMode, CircuitPythonMode, DebugMode, PyGameZeroMode):
        host.register_mode(mode_class)
    host.plugin_records = load_plugins(host)
    plugin_errors.install(host._error_handler)
    return host.modes

show_crash_dialog(details)

Show the crash details so the user can copy them. Nothing is sent anywhere. Skipped if there's no QApplication yet (a crash during start-up) or MU_SUPPRESS_CRASH_REPORT_FORM is set (tests and CI).

Source code in mu/app.py
def show_crash_dialog(details):
    """
    Show the crash details so the user can copy them. Nothing is sent
    anywhere. Skipped if there's no QApplication yet (a crash during
    start-up) or MU_SUPPRESS_CRASH_REPORT_FORM is set (tests and CI).
    """
    if "MU_SUPPRESS_CRASH_REPORT_FORM" in os.environ:
        return
    if QApplication.instance() is None:
        return
    CrashDialog(details).exec()

mu.logic

Most of the fundamental logic for Mu is in this module.

mu.logic

Copyright (c) 2015-2017 Nicholas H.Tollervey and others (see the AUTHORS file).

Based upon work done for Puppy IDE by Dan Pope, Nicholas Tollervey and Damien George.

This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/.

Device

Device object, containing both information about the connected device, the port it's connected through and the mode it works with.

Source code in mu/logic.py
class Device:
    """
    Device object, containing both information about the connected device,
    the port it's connected through and the mode it works with.
    """

    def __init__(
        self,
        vid,
        pid,
        port,
        serial_number,
        manufacturer,
        long_mode_name,
        short_mode_name,
        board_name=None,
    ):
        self.vid = vid
        self.pid = pid
        self.port = port
        self.serial_number = serial_number
        self.manufacturer = manufacturer
        self.long_mode_name = long_mode_name
        self.short_mode_name = short_mode_name
        self.board_name = board_name

    @property
    def name(self):
        """
        Returns the device name.
        """
        if self.board_name:
            return self.board_name
        else:
            return _("{} compatible").format(self.long_mode_name)

    def __eq__(self, other):
        """
        Equality on devices. Comparison on vid, pid, and serial_number,
        and most importantly also matches on which port the device is
        connected to. That is, if two identical devices are connected
        to separate ports they are considered different.
        """
        return (
            isinstance(other, self.__class__)
            and self.pid == other.pid
            and self.vid == other.vid
            and self.port == other.port
            and self.serial_number == other.serial_number
        )

    def __ne__(self, other):
        """
        Inequality of devices is the negation of equality
        """
        return not self.__eq__(other)

    def __lt__(self, other):
        """
        Alphabetical ordering according to device name
        """
        return self.name < other.name

    def __gt__(self, other):
        """
        Alphabetical ordering according to device name
        """
        return self.name > other.name

    def __le__(self, other):
        """
        Alphabetical ordering according to device name
        """
        return self.name <= other.name

    def __ge__(self, other):
        """
        Alphabetical ordering according to device name
        """
        return self.name >= other.name

    def __str__(self):
        """
        String representation of devices includes name, port, and VID/PID
        """
        s = "{} on {} (VID: 0x{:04X}, PID: 0x{:04X})"
        return s.format(self.name, self.port, self.vid, self.pid)

    def __hash__(self):
        """
        Hash is the hash of the string representation, includes the same
        elements in the hash as in equality testing.
        """
        return hash(str(self))

name property

Returns the device name.

__eq__(other)

Equality on devices. Comparison on vid, pid, and serial_number, and most importantly also matches on which port the device is connected to. That is, if two identical devices are connected to separate ports they are considered different.

Source code in mu/logic.py
def __eq__(self, other):
    """
    Equality on devices. Comparison on vid, pid, and serial_number,
    and most importantly also matches on which port the device is
    connected to. That is, if two identical devices are connected
    to separate ports they are considered different.
    """
    return (
        isinstance(other, self.__class__)
        and self.pid == other.pid
        and self.vid == other.vid
        and self.port == other.port
        and self.serial_number == other.serial_number
    )

__ge__(other)

Alphabetical ordering according to device name

Source code in mu/logic.py
def __ge__(self, other):
    """
    Alphabetical ordering according to device name
    """
    return self.name >= other.name

__gt__(other)

Alphabetical ordering according to device name

Source code in mu/logic.py
def __gt__(self, other):
    """
    Alphabetical ordering according to device name
    """
    return self.name > other.name

__hash__()

Hash is the hash of the string representation, includes the same elements in the hash as in equality testing.

Source code in mu/logic.py
def __hash__(self):
    """
    Hash is the hash of the string representation, includes the same
    elements in the hash as in equality testing.
    """
    return hash(str(self))

__le__(other)

Alphabetical ordering according to device name

Source code in mu/logic.py
def __le__(self, other):
    """
    Alphabetical ordering according to device name
    """
    return self.name <= other.name

__lt__(other)

Alphabetical ordering according to device name

Source code in mu/logic.py
def __lt__(self, other):
    """
    Alphabetical ordering according to device name
    """
    return self.name < other.name

__ne__(other)

Inequality of devices is the negation of equality

Source code in mu/logic.py
def __ne__(self, other):
    """
    Inequality of devices is the negation of equality
    """
    return not self.__eq__(other)

__str__()

String representation of devices includes name, port, and VID/PID

Source code in mu/logic.py
def __str__(self):
    """
    String representation of devices includes name, port, and VID/PID
    """
    s = "{} on {} (VID: 0x{:04X}, PID: 0x{:04X})"
    return s.format(self.name, self.port, self.vid, self.pid)

DeviceList

Bases: QAbstractListModel

Source code in mu/logic.py
class DeviceList(QtCore.QAbstractListModel):
    device_connected = pyqtSignal("PyQt_PyObject")
    device_disconnected = pyqtSignal("PyQt_PyObject")

    def __init__(self, modes, parent=None):
        super().__init__(parent)
        self.modes = modes
        self._devices = list()

    def __iter__(self):
        """
        Enables iteration over the list of devices
        """
        return iter(self._devices)

    def __getitem__(self, i):
        """
        Enable [] operator
        """
        return self._devices[i]

    def __len__(self):
        """
        Number of devices
        """
        return len(self._devices)

    def rowCount(self, parent):
        """
        Number of devices
        """
        return len(self._devices)

    def data(self, index, role):
        """
        Reimplements QAbstractListModel.data(): returns data for the
        specified index and role. In this case only implemented for
        ToolTipRole and DisplayRole
        """
        device = self._devices[index.row()]
        if role == QtCore.Qt.ItemDataRole.ToolTipRole:
            return str(device)
        elif role == QtCore.Qt.ItemDataRole.DisplayRole:
            return device.name

    def add_device(self, new_device):
        """
        Add a new device to the device list, maintains alphabetical ordering
        """
        parent = QtCore.QModelIndex()
        # Find position to insert sorted
        position = 0
        for i, device in enumerate(self._devices):
            if new_device > device:
                position = i + 1
        # Insert
        self.beginInsertRows(parent, position, position)
        self._devices.insert(position, new_device)
        self.endInsertRows()

    def remove_device(self, device):
        """
        Remove the given device from the device list
        """
        parent = QtCore.QModelIndex()
        position = self._devices.index(device)
        self.beginRemoveRows(parent, position, position)
        self._devices.remove(device)
        self.endRemoveRows()

    def check_usb(self):
        """
        Ensure connected USB devices are polled. If there's a change and a new
        recognised device is attached, inform the user via a status message.
        If a single device is found and Mu is in a different mode ask the user
        if they'd like to change mode.
        """
        devices = []
        device_types = set()
        # Detect connected devices.
        for mode_name, mode in self.modes.items():
            if hasattr(mode, "find_devices"):
                # The mode can detect attached devices. This runs every
                # second for every mode, including plugin modes the user
                # isn't using, so a failure is logged rather than fatal.
                try:
                    detected = mode.find_devices(with_logging=False)
                except Exception:
                    logger.exception("Mode %s failed to look for devices", mode_name)
                    continue
                if detected:
                    device_types.add(mode_name)
                    devices.extend(detected)
        # Remove no-longer connected devices.
        for device in self:
            if device not in devices:
                self.remove_device(device)
                self.device_disconnected.emit(device)
                logger.info(
                    (
                        "{} device disconnected on port: {}"
                        "(VID: 0x{:04X}, PID: 0x{:04X}, manufacturer {})"
                    ).format(
                        device.short_mode_name,
                        device.port,
                        device.vid,
                        device.pid,
                        device.manufacturer,
                    )
                )
        # Add newly connected devices.
        for device in devices:
            if device not in self:
                self.add_device(device)
                self.device_connected.emit(device)
                logger.info(
                    (
                        "{} device connected on port: {}"
                        "(VID: 0x{:04X}, PID: 0x{:04X}, manufacturer: '{}')"
                    ).format(
                        device.short_mode_name,
                        device.port,
                        device.vid,
                        device.pid,
                        device.manufacturer,
                    )
                )

__getitem__(i)

Enable [] operator

Source code in mu/logic.py
def __getitem__(self, i):
    """
    Enable [] operator
    """
    return self._devices[i]

__iter__()

Enables iteration over the list of devices

Source code in mu/logic.py
def __iter__(self):
    """
    Enables iteration over the list of devices
    """
    return iter(self._devices)

__len__()

Number of devices

Source code in mu/logic.py
def __len__(self):
    """
    Number of devices
    """
    return len(self._devices)

add_device(new_device)

Add a new device to the device list, maintains alphabetical ordering

Source code in mu/logic.py
def add_device(self, new_device):
    """
    Add a new device to the device list, maintains alphabetical ordering
    """
    parent = QtCore.QModelIndex()
    # Find position to insert sorted
    position = 0
    for i, device in enumerate(self._devices):
        if new_device > device:
            position = i + 1
    # Insert
    self.beginInsertRows(parent, position, position)
    self._devices.insert(position, new_device)
    self.endInsertRows()

check_usb()

Ensure connected USB devices are polled. If there's a change and a new recognised device is attached, inform the user via a status message. If a single device is found and Mu is in a different mode ask the user if they'd like to change mode.

Source code in mu/logic.py
def check_usb(self):
    """
    Ensure connected USB devices are polled. If there's a change and a new
    recognised device is attached, inform the user via a status message.
    If a single device is found and Mu is in a different mode ask the user
    if they'd like to change mode.
    """
    devices = []
    device_types = set()
    # Detect connected devices.
    for mode_name, mode in self.modes.items():
        if hasattr(mode, "find_devices"):
            # The mode can detect attached devices. This runs every
            # second for every mode, including plugin modes the user
            # isn't using, so a failure is logged rather than fatal.
            try:
                detected = mode.find_devices(with_logging=False)
            except Exception:
                logger.exception("Mode %s failed to look for devices", mode_name)
                continue
            if detected:
                device_types.add(mode_name)
                devices.extend(detected)
    # Remove no-longer connected devices.
    for device in self:
        if device not in devices:
            self.remove_device(device)
            self.device_disconnected.emit(device)
            logger.info(
                (
                    "{} device disconnected on port: {}"
                    "(VID: 0x{:04X}, PID: 0x{:04X}, manufacturer {})"
                ).format(
                    device.short_mode_name,
                    device.port,
                    device.vid,
                    device.pid,
                    device.manufacturer,
                )
            )
    # Add newly connected devices.
    for device in devices:
        if device not in self:
            self.add_device(device)
            self.device_connected.emit(device)
            logger.info(
                (
                    "{} device connected on port: {}"
                    "(VID: 0x{:04X}, PID: 0x{:04X}, manufacturer: '{}')"
                ).format(
                    device.short_mode_name,
                    device.port,
                    device.vid,
                    device.pid,
                    device.manufacturer,
                )
            )

data(index, role)

Reimplements QAbstractListModel.data(): returns data for the specified index and role. In this case only implemented for ToolTipRole and DisplayRole

Source code in mu/logic.py
def data(self, index, role):
    """
    Reimplements QAbstractListModel.data(): returns data for the
    specified index and role. In this case only implemented for
    ToolTipRole and DisplayRole
    """
    device = self._devices[index.row()]
    if role == QtCore.Qt.ItemDataRole.ToolTipRole:
        return str(device)
    elif role == QtCore.Qt.ItemDataRole.DisplayRole:
        return device.name

remove_device(device)

Remove the given device from the device list

Source code in mu/logic.py
def remove_device(self, device):
    """
    Remove the given device from the device list
    """
    parent = QtCore.QModelIndex()
    position = self._devices.index(device)
    self.beginRemoveRows(parent, position, position)
    self._devices.remove(device)
    self.endRemoveRows()

rowCount(parent)

Number of devices

Source code in mu/logic.py
def rowCount(self, parent):
    """
    Number of devices
    """
    return len(self._devices)

Editor

Bases: QObject

Application logic for the editor itself.

Source code in mu/logic.py
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
class Editor(QObject):
    """
    Application logic for the editor itself.
    """

    def __init__(self, view):
        super().__init__()
        logger.info("Setting up editor.")
        self._view = view
        self.fs = None
        self.theme = "day"
        self.mode = "python"
        self.python_extensions = [".py", ".pyw"]
        self.modes = {}
        self.envars = {}  # See restore session and show_admin
        self.user_locale = ""  # user defined language locale
        self.connected_devices = DeviceList(self.modes, parent=self)
        self.current_device = None
        self.find = ""
        self.replace = ""
        self.current_path = ""  # Directory of last loaded file.
        self.global_replace = False
        self.selecting_mode = False  # Flag to stop auto-detection of modes.
        if not os.path.exists(DATA_DIR):
            logger.debug("Creating directory: {}".format(DATA_DIR))
            os.makedirs(DATA_DIR)
        logger.info("Log directory: {}".format(LOG_DIR))
        logger.info("Data directory: {}".format(DATA_DIR))

        @view.open_file.connect
        def on_open_file(file):
            # Open the file
            self.direct_load(file)

    def setup(self, modes):
        """
        Define the available modes and ensure there's a default working
        directory.
        """
        self.modes = modes
        self.connected_devices.modes = modes
        logger.info("Available modes: {}".format(", ".join(self.modes.keys())))
        # Ensure there is a workspace directory.
        wd = self.modes["python"].workspace_dir()
        if not os.path.exists(wd):
            logger.debug("Creating directory: {}".format(wd))
            os.makedirs(wd)
        # Ensure PyGameZero assets are copied over.
        images_path = os.path.join(wd, "images")
        fonts_path = os.path.join(wd, "fonts")
        sounds_path = os.path.join(wd, "sounds")
        music_path = os.path.join(wd, "music")
        if not os.path.exists(images_path):
            logger.debug("Creating directory: {}".format(images_path))
            os.makedirs(images_path)
            for img in DEFAULT_IMAGES:
                shutil.copy(path(img, "pygamezero/"), os.path.join(images_path, img))
        if not os.path.exists(fonts_path):
            logger.debug("Creating directory: {}".format(fonts_path))
            os.makedirs(fonts_path)
        if not os.path.exists(sounds_path):
            logger.debug("Creating directory: {}".format(sounds_path))
            os.makedirs(sounds_path)
            for sfx in DEFAULT_SOUNDS:
                shutil.copy(path(sfx, "pygamezero/"), os.path.join(sounds_path, sfx))
        if not os.path.exists(music_path):
            logger.debug("Creating directory: {}".format(music_path))
            os.makedirs(music_path)
        # Ensure Web based assets are copied over.
        template_path = os.path.join(wd, "templates")
        static_path = os.path.join(wd, "static")
        if not os.path.exists(template_path):
            logger.debug("Creating directory: {}".format(template_path))
            shutil.copytree(path("templates", "web/"), template_path)
        if not os.path.exists(static_path):
            logger.debug("Creating directory: {}".format(static_path))
            shutil.copytree(path("static", "web/"), static_path)
            # Copy all the static directories.
        # Start the timer to poll every second for an attached or removed
        # USB device.
        self._view.set_usb_checker(1, self.connected_devices.check_usb)

    def connect_to_status_bar(self, status_bar):
        """
        Connect the editor with the Window-statusbar.
        Should be called after Editor.setup(), to ensure modes are initialized
        """
        # Connect to logs
        status_bar.connect_logs(self.show_admin, "Ctrl+Shift+D")
        # Show connection messages in status_bar
        self.connected_devices.device_connected.connect(status_bar.device_connected)
        # Connect to device list
        device_selector = status_bar.device_selector
        status_bar.device_selector.set_device_list(self.connected_devices)
        # Propagate device_changed events
        device_selector.device_changed.connect(self.device_changed)
        if self.modes:
            for mode in self.modes.values():
                device_selector.device_changed.connect(mode.device_changed)

    def restore_session(self, paths=None):
        """
        Attempts to recreate the tab state from the last time the editor was
        run. If paths contains a collection of additional paths specified by
        the user, they are also "restored" at the same time (duplicates will be
        ignored).
        """
        self.change_mode(self.mode)
        old_session = settings.session
        logger.debug(old_session)
        if "theme" in old_session:
            self.theme = old_session["theme"]
        self._view.set_theme(self.theme)
        if "mode" in old_session:
            old_mode = old_session["mode"]
            if old_mode in self.modes:
                self.mode = old_session["mode"]
            else:
                # Unknown mode (perhaps an old version?)
                self.select_mode(None)
        else:
            # So ask for the desired mode.
            self.select_mode(None)
        if "paths" in old_session:
            old_paths = self._abspath(old_session["paths"])
            launch_paths = self._abspath(paths) if paths else set()
            for old_path in old_paths:
                # if the os passed in a file, defer loading it now
                if old_path in launch_paths:
                    continue
                self.direct_load(old_path)
            logger.info("Loaded files.")
        if "envars" in old_session:
            old_envars = old_session["envars"]
            if isinstance(old_envars, list):
                old_envars = dict(old_envars)
            self.envars = old_envars
            logger.info("User defined environment variables: {}".format(self.envars))
        if "zoom_level" in old_session:
            self._view.zoom_position = old_session["zoom_level"]
            self._view.set_zoom()

        if "venv_path" in old_session:
            venv.relocate(old_session["venv_path"])
            venv.ensure()

        if "locale" in old_session:
            self.user_locale = old_session["locale"].strip()
            if self.user_locale:
                i18n.set_language(self.user_locale)

        old_window = old_session.get("window", {})
        self._view.size_window(**old_window)

        #
        # Doesn't seem to do anything useful
        #
        # ~ if old_session is None:
        # ~ self._view.set_theme(self.theme)

        # handle os passed file last,
        # so it will not be focused over by another tab
        if paths and len(paths) > 0:
            self.load_cli(paths)
        self.change_mode(self.mode)
        self.show_status_message(random.choice(MOTD), 10)
        if not self._view.tab_count:
            py = self.modes[self.mode].code_template + NEWLINE
            tab = self._view.add_tab(None, py, self.modes[self.mode].api(), NEWLINE)
            tab.setCursorPosition(len(py.split(NEWLINE)), 0)
            logger.info("Starting with blank file.")

    def toggle_theme(self):
        """
        Switches between themes (night, day or high-contrast).
        """
        if self.theme == "day":
            self.theme = "night"
        elif self.theme == "night":
            self.theme = "contrast"
        else:
            self.theme = "day"
        logger.info("Toggle theme to: {}".format(self.theme))
        self._view.set_theme(self.theme)

    def new(self):
        """
        Adds a new tab to the editor.
        """
        logger.info("Added a new tab.")
        default_text = self.modes[self.mode].code_template + NEWLINE
        self._view.add_tab(None, default_text, self.modes[self.mode].api(), NEWLINE)

    def _load(self, path):
        """
        Attempt to load a Python script from the passed in path. This path may
        be a .py file containing Python source code, or a .hex file, created
        for a micro:bit like device, with the source code embedded therein.

        This method will work its way around duplicate paths and also attempt
        to cleanly handle / report / log errors when encountered in a helpful
        manner.
        """
        logger.info("Loading script from: {}".format(path))
        error = _(
            "The file contains characters Nu expects to be encoded as "
            "{0} or as the computer's default encoding {1}, but which "
            "are encoded in some other way.\n\nIf this file was saved "
            "in another application, re-save the file via the "
            "'Save as' option and set the encoding to {0}"
        )
        error = error.format(ENCODING, locale.getpreferredencoding())
        # Does the file even exist?
        if not os.path.isfile(path):
            logger.info("The file {} does not exist.".format(path))
            return
        # see if file is open first
        for widget in self._view.widgets:
            if widget.path is None:  # this widget is an unsaved buffer
                continue
            # The widget could be for a file on a MicroPython device that
            # has since been unplugged. We should ignore it and assume that
            # folks understand this file is no longer available (there's
            # nothing else we can do).
            if not os.path.isfile(widget.path):
                logger.info("The file {} no longer exists.".format(widget.path))
                continue
            # Check for duplication of open file.
            if os.path.samefile(path, widget.path):
                logger.info("Script already open.")
                msg = _('The file "{}" is already open.')
                self._view.show_message(msg.format(os.path.basename(path)))
                self._view.focus_tab(widget)
                return
        name, text, newline, file_mode = None, None, None, None
        try:
            if self.has_python_extension(path):
                # Open the file, read the textual content and set the name as
                # the path to the file.
                try:
                    text, newline = read_and_decode(path)
                except UnicodeDecodeError:
                    message = _("Nu cannot read the characters in {}")
                    filename = os.path.basename(path)
                    self._view.show_message(message.format(filename), error)
                    return
                name = path
            else:
                # Delegate the open operation to the Mu modes. Leave the name
                # as None if handling a hex file, thus forcing the user to work
                # out what to name the recovered script.
                for mode_name, mode in self.modes.items():
                    try:
                        text, newline = mode.open_file(path)
                        if not path.endswith(".hex"):
                            name = path
                    except Exception as exc:
                        # No worries, log it and try the next mode
                        logger.warning(
                            "Error when mode {} try to open the {} file.".format(
                                mode_name, path
                            ),
                            exc_info=exc,
                        )
                    else:
                        if text:
                            file_mode = mode_name
                            break
                else:
                    message = _("Nu was not able to open this file")
                    info = _(
                        "Currently Nu only works with Python source "
                        "files or hex files created with embedded "
                        "MicroPython code."
                    )
                    self._view.show_message(message, info)
                    return
        except OSError:
            message = _("Could not load {}").format(path)
            logger.exception("Could not load {}".format(path))
            info = _(
                "Does this file exist?\nIf it does, do you have "
                "permission to read it?\n\nPlease check and try again."
            )
            self._view.show_message(message, info)
        else:
            if file_mode and self.mode != file_mode:
                mode_name = self.modes[file_mode].name
                message = _("Is this a {} file?").format(mode_name)
                info = _(
                    "It looks like this could be a {} file.\n\n"
                    "Would you like to change Nu to the {}"
                    "mode?"
                ).format(mode_name, mode_name)
                if (
                    self._view.show_confirmation(message, info, icon="Question")
                    == QMessageBox.StandardButton.Ok
                ):
                    self.change_mode(file_mode)
            logger.debug(text)
            self._view.add_tab(name, text, self.modes[self.mode].api(), newline)

    def get_dialog_directory(self, default=None):
        """
        Return the directory folder which a load/save dialog box should
        open into. In order of precedence this function will return:

        0) If not None, the value of default.
        1) The last location used by a load/save dialog.
        2) The directory containing the current file.
        3) The mode's reported workspace directory.
        """
        if default is not None:
            folder = default
        elif self.current_path and os.path.isdir(self.current_path):
            folder = self.current_path
        else:
            current_file_path = ""
            try:
                workspace_path = self.modes[self.mode].workspace_dir()
            except Exception as e:
                # Avoid crashing if workspace_dir raises, use default path
                # instead
                workspace_path = self.modes["python"].workspace_dir()
                logger.error(
                    (
                        "Could not open {} mode workspace directory"
                        'due to exception "{}". Using:'
                        "\n\n{}\n\n...to store your code instead"
                    ).format(self.mode, e, workspace_path)
                )
            tab = self._view.current_tab
            if tab and tab.path:
                current_file_path = os.path.dirname(os.path.abspath(tab.path))
            folder = current_file_path if current_file_path else workspace_path
        logger.info("Using path for file dialog: {}".format(folder))
        return folder

    def load(self, *args, default_path=None):
        """
        Loads a Python (or other supported) file from the file system or
        extracts a Python script from a hex file.
        """
        # Get all supported extensions from the different modes
        extensions = [ext.strip("*.") for ext in self.python_extensions]
        for mode_name, mode in self.modes.items():
            if mode.file_extensions:
                extensions += mode.file_extensions
        extensions = [e.lower() for e in extensions]
        extensions = "*.{} *.{}".format(
            " *.".join(extensions), " *.".join(extensions).upper()
        )
        folder = self.get_dialog_directory(default_path)
        allow_previous = not bool(default_path)
        path = self._view.get_load_path(
            folder, extensions, allow_previous=allow_previous
        )
        if path:
            self.current_path = os.path.dirname(os.path.abspath(path))
            self._load(path)

    def direct_load(self, path):
        """
        For loading files passed from command line or the OS launch.
        """
        self._load(path)

    def load_cli(self, paths):
        """
        Given a set of paths, passed in by the user when Mu starts, this
        method will attempt to load them and log / report a problem if Mu is
        unable to open a passed in path.
        """
        for p in paths:
            try:
                logger.info("Passed-in filename: {}".format(p))
                # abspath will fail for non-paths
                self.direct_load(os.path.abspath(p))
            except Exception as e:
                logging.warning(
                    "Can't open file from command line {}".format(p),
                    exc_info=e,
                )

    def _abspath(self, paths):
        """
        Safely convert an array of paths to their absolute forms and remove
        duplicate items.
        """
        result = []
        for p in paths:
            try:
                abspath = os.path.abspath(p)
            except Exception as ex:
                logger.error("Could not get path for {}: {}".format(p, ex))
            else:
                if abspath not in result:
                    result.append(abspath)
        return result

    def save_tab_to_file(self, tab, show_error_messages=True):
        """
        Given a tab, will attempt to save the script in the tab to the path
        associated with the tab. If there's a problem this will be logged and
        reported and the tab status will continue to show as Modified.
        """
        logger.info("Saving script to: {}".format(tab.path))
        logger.debug(tab.text())
        try:
            save_and_encode(tab.text(), tab.path, tab.newline)
        except OSError as e:
            logger.error(e)
            error_message = _("Could not save file (disk problem)")
            information = _(
                "Error saving file to disk. Ensure you have "
                "permission to write the file and "
                "sufficient disk space."
            )
        except UnicodeEncodeError:
            error_message = _("Could not save file (encoding problem)")
            logger.exception(error_message)
            information = _(
                "Unable to convert all the characters. If you "
                "have an encoding line at the top of the file, "
                "remove it and try again."
            )
        else:
            error_message = information = None
        if error_message and show_error_messages:
            self._view.show_message(error_message, information)
        else:
            tab.setModified(False)
            self.show_status_message(_("Saved file: {}").format(tab.path))

    def check_for_shadow_module(self, path):
        """
        Check if the filename in the path is a shadow of a module already in
        the Python path. For example, many learners will save their first
        turtle based script as turtle.py, thus causing Python to never find
        the built in turtle module because of the name conflict.

        If the filename shadows an existing module, return True, otherwise,
        return False.
        """
        logger.info('Checking path "{}" for shadow module.'.format(path))
        pyextensions = [".pyw", ".PYW", ".py", ".PY"]
        filename = os.path.basename(path)
        for ext in pyextensions:
            filename = filename.replace(ext, "")
        return filename in self.modes[self.mode].module_names

    def save(self, *args, default=None):
        """
        Save the content of the currently active editor tab.
        """
        tab = self._view.current_tab
        if tab is None:
            # There is no active text editor so abort.
            return
        if not tab.path:
            # Unsaved file.
            folder = self.get_dialog_directory(default)
            path = self._view.get_save_path(folder)
            if path and self.check_for_shadow_module(path):
                message = _('You cannot use the filename "{}"').format(
                    os.path.basename(path)
                )
                info = _(
                    "This name is already used by another part of "
                    "Python. If you use this name, things are "
                    "likely to break. Please try again with a "
                    "different filename."
                )
                self._view.show_message(message, info)
                return
            tab.path = path
        if tab.path:
            # The user specified a path to a file.
            self.save_tab_to_file(tab)
        else:
            # The user cancelled the filename selection.
            tab.path = None

    def get_tab(self, path):
        """
        Given a path, returns either an existing tab for the path or creates /
        loads a new tab for the path.
        """
        normalised_path = os.path.normcase(os.path.abspath(path))
        for tab in self._view.widgets:
            if tab.path:
                tab_path = os.path.normcase(os.path.abspath(tab.path))
                if tab_path == normalised_path:
                    self._view.focus_tab(tab)
                    return tab
        self.direct_load(path)
        return self._view.current_tab

    def zoom_in(self):
        """
        Make the editor's text bigger
        """
        logger.info("Zoom in")
        self._view.zoom_in()

    def zoom_out(self):
        """
        Make the editor's text smaller.
        """
        logger.info("Zoom out")
        self._view.zoom_out()

    def check_code(self):
        """
        Uses PyFlakes and PyCodeStyle to gather information about potential
        problems with the code in the current tab.
        """
        tab = self._view.current_tab
        if tab is None:
            # There is no active text editor so abort.
            return
        if tab.path and not self.has_python_extension(tab.path):
            # Only works on Python files, so abort.
            return
        tab.has_annotations = not tab.has_annotations
        if tab.has_annotations:
            logger.info("Checking code.")
            self._view.reset_annotations()
            filename = tab.path if tab.path else _("untitled")
            builtins = self.modes[self.mode].builtins
            flake = check_flake(filename, tab.text(), builtins)
            if flake:
                logger.info(flake)
                self._view.annotate_code(flake, "error")
            pep8 = check_pycodestyle(tab.text())
            if pep8:
                logger.info(pep8)
                self._view.annotate_code(pep8, "style")
            self._view.show_annotations()
            tab.has_annotations = bool(flake or pep8)
            if not tab.has_annotations:
                # No problems detected, so confirm this with a friendly
                # message.
                ok_messages = [
                    _("Good job! No problems found."),
                    _("Hurrah! Checker turned up no problems."),
                    _("Nice one! Zero problems detected."),
                    _("Well done! No problems here."),
                    _("Awesome! Zero problems found."),
                ]
                self.show_status_message(random.choice(ok_messages))
                self._view.set_checker_icon("check-good")
            else:
                self._view.set_checker_icon("check-bad")
        else:
            self._view.reset_annotations()

    def show_help(self):
        """
        Display browser based help about Nu (docs/help.md on the docs site).
        """
        logger.info("Showing help at %r.", HELP_URL)
        webbrowser.open_new(HELP_URL)

    def quit(self, *args, **kwargs):
        """
        Exit the application.
        """
        if self._view.modified:
            # Alert the user to handle unsaved work.
            msg = _(
                "There is un-saved work, exiting the application will"
                " cause you to lose it."
            )
            result = self._view.show_confirmation(msg)
            if result == QMessageBox.StandardButton.Cancel:
                if args and hasattr(args[0], "ignore"):
                    # The function is handling an event, so ignore it.
                    args[0].ignore()
                return
        paths = []
        for widget in self._view.widgets:
            if widget.path:
                paths.append(os.path.abspath(widget.path))
        # Make sure the mode's stop method is called so
        # everything is cleaned up.
        self.modes[self.mode].stop()
        session = {
            "theme": self.theme,
            "mode": self.mode,
            "paths": paths,
            "envars": self.envars,
            "zoom_level": self._view.zoom_position,
            "window": {
                "x": self._view.x(),
                "y": self._view.y(),
                "w": self._view.width(),
                "h": self._view.height(),
            },
            "locale": self.user_locale,
        }
        save_session(session)
        logger.info("Quitting.\n\n")
        QtCore.QCoreApplication.exit(0)

    def show_admin(self, event=None):
        """
        Cause the editor's admin dialog to be displayed to the user.

        Ensure any changes to the envars is updated.
        """
        logger.info("Showing admin with logs from {}".format(LOG_FILE))
        envars = "\n".join(
            ["{}={}".format(name, value) for name, value in self.envars.items()]
        )
        settings = {
            "envars": envars,
            "locale": self.user_locale,
        }
        baseline_packages, user_packages = venv.installed_packages()
        packages = user_packages
        with open(LOG_FILE, "r", encoding="utf8") as logfile:
            new_settings = self._view.show_admin(
                logfile.read(),
                settings,
                "\n".join(packages),
                self.modes[self.mode],
                self.connected_devices,
            )
        if new_settings:
            if "envars" in new_settings:
                self.envars = extract_envars(new_settings["envars"])
            if "packages" in new_settings:
                new_packages = [
                    p for p in new_settings["packages"].lower().split("\n") if p.strip()
                ]
                old_packages = [p.lower() for p in user_packages]
                self.sync_package_state(old_packages, new_packages)
            if "locale" in new_settings:
                self.user_locale = new_settings["locale"]
            # Ensure mode UI is updated given settings.
            self.modes[self.mode].ensure_state()
        else:
            logger.info("No admin settings changed.")

    def sync_package_state(self, old_packages, new_packages):
        """
        Given the state of the old third party packages, compared to the new
        third party packages, ensure that pip uninstalls and installs the
        packages so the currently available third party packages reflects the
        new state.
        """
        old = set(old_packages)
        new = set(new_packages)
        logger.info("Synchronize package states...")
        logger.info("Old: {}".format(old))
        logger.info("New: {}".format(new))
        to_remove = old.difference(new)
        to_add = new.difference(old)
        if to_remove or to_add:
            logger.info("To add: {}".format(to_add))
            logger.info("To remove: {}".format(to_remove))
            logger.info("Virtualenv: {}".format(VENV_DIR))
            self._view.sync_packages(to_remove, to_add)

    def select_mode(self, event=None):
        """
        Select the mode that editor is supposed to be in.
        """
        if self.modes[self.mode].is_debugger:
            return
        logger.info("Showing available modes: {}".format(list(self.modes.keys())))
        self.selecting_mode = True  # Flag to stop auto-detection of modes.
        new_mode = self._view.select_mode(self.modes, self.mode)
        self.selecting_mode = False
        if new_mode and new_mode != self.mode:
            logger.info("New mode selected: {}".format(new_mode))
            self.change_mode(new_mode)

    def change_mode(self, mode):
        """
        Given the name of a mode, will make the necessary changes to put the
        editor into the new mode.
        """
        # Remove the old mode's REPL / filesystem / plotter if required.
        old_mode = self.modes[self.mode]
        if hasattr(old_mode, "remove_repl"):
            old_mode.remove_repl()
        if hasattr(old_mode, "remove_fs"):
            old_mode.remove_fs()
        if hasattr(old_mode, "remove_plotter"):
            if old_mode.plotter:
                old_mode.remove_plotter()
        # Deactivate old mode
        self.modes[self.mode].deactivate()
        # Re-assign to new mode.
        self.mode = mode
        # Activate new mode
        self.modes[mode].activate()
        # Update buttons.
        self._view.change_mode(self.modes[mode])
        button_bar = self._view.button_bar
        button_bar.connect("modes", self.select_mode, "Ctrl+Shift+M")
        button_bar.connect("new", self.new, "Ctrl+N")
        button_bar.connect("load", self.load, "Ctrl+O")
        button_bar.connect("save", self.save, "Ctrl+S")
        for action in self.modes[mode].actions():
            button_bar.connect(action["name"], action["handler"], action["shortcut"])
        button_bar.connect("zoom-in", self.zoom_in, "Ctrl++")
        button_bar.connect("zoom-out", self.zoom_out, "Ctrl+-")
        button_bar.connect("theme", self.toggle_theme, "F1")
        button_bar.connect("check", self.check_code, "F2")
        if sys.version_info[:2] >= (3, 6):
            button_bar.connect("tidy", self.tidy_code, "F10")
        button_bar.connect("help", self.show_help, "Ctrl+H")
        button_bar.connect("quit", self.quit, "Ctrl+Q")
        self._view.status_bar.set_mode(self.modes[mode].name)
        # Update references to default file locations.
        try:
            workspace_dir = self.modes[mode].workspace_dir()
            logger.info("Workspace directory: {}".format(workspace_dir))
        except Exception as e:
            # Avoid crashing if workspace_dir raises, use default path instead
            workspace_dir = self.modes["python"].workspace_dir()
            logger.error(
                (
                    "Could not open {} mode workspace directory, "
                    'due to exception "{}".'
                    "Using:\n\n{}\n\n...to store your code instead"
                ).format(mode, repr(e), workspace_dir)
            )
        # Reset remembered current path for load/save dialogs.
        self.current_path = ""
        # Ensure auto-save timeouts are set.
        if self.modes[mode].save_timeout > 0:
            # Start the timer
            self._view.set_timer(self.modes[mode].save_timeout, self.autosave)
        else:
            # Stop the timer
            self._view.stop_timer()
        # Update breakpoint states.
        if not (self.modes[mode].is_debugger or self.modes[mode].has_debugger):
            for tab in self._view.widgets:
                tab.breakpoint_handles = set()
                tab.reset_annotations()
        self.modes[mode].ensure_state()
        self.show_status_message(_("Changed to {} mode.").format(self.modes[mode].name))

    def autosave(self):
        """
        Cycles through each tab and, if changed, saves it to the filesystem.
        """
        if self._view.modified:
            # Something has changed, so save it!
            for tab in self._view.widgets:
                if tab.path and tab.isModified():
                    # Suppress error message on autosave attempts
                    self.save_tab_to_file(tab, show_error_messages=False)
                    logger.info(
                        "Autosave detected and saved changes in {}.".format(tab.path)
                    )

    def ask_to_change_mode(self, new_mode, mode_name, heading):
        """
        Open a dialog asking the user, whether to change mode from
        mode_name to new_mode. The dialog can be customized by the
        heading-parameter.
        """
        # Only ask to switch mode if we're not already trying to
        # select a new mode via the dialog. Cannot change mode if
        # a script is already being run by the current mode.
        m = self.modes[self.mode]
        running = hasattr(m, "runner") and m.runner
        if (self.mode != new_mode and not self.selecting_mode) and not running:
            msg_body = _("Would you like to change Nu to the {} mode?").format(
                mode_name
            )
            change_confirmation = self._view.show_confirmation(
                heading, msg_body, icon="Question"
            )
            if change_confirmation == QMessageBox.StandardButton.Ok:
                self.change_mode(new_mode)

    def device_changed(self, device):
        """
        Slot for receiving signals that the current device has changed.
        If the device change requires mode change, the user will be
        asked through a dialog.
        """
        if device:
            if self.current_device is None:
                heading = _("Detected new {} device.").format(device.name)
            else:
                heading = _("Device changed to {}.").format(device.name)
            self.ask_to_change_mode(
                device.short_mode_name, device.long_mode_name, heading
            )
        self.current_device = device

    def show_status_message(self, message, duration=5):
        """
        Displays the referenced message for duration seconds.
        """
        self._view.status_bar.set_message(message, duration * 1000)

    def debug_toggle_breakpoint(self, margin, line, modifiers):
        """
        How to handle the toggling of a breakpoint.
        """
        if self.modes[self.mode].has_debugger or self.modes[self.mode].is_debugger:
            tab = self._view.current_tab
            code = tab.text(line)
            if self.mode == "debugger":
                # The debugger is running.
                if is_breakpoint_line(code):
                    self.modes["debugger"].toggle_breakpoint(line, tab)
                    return
            else:
                # The debugger isn't running.
                if tab.markersAtLine(line):
                    tab.markerDelete(line, -1)
                    return
                elif is_breakpoint_line(code):
                    handle = tab.markerAdd(line, tab.BREAKPOINT_MARKER)
                    tab.breakpoint_handles.add(handle)
                    return
            msg = _("Cannot Set Breakpoint.")
            info = _(
                "Lines that are comments or some multi-line "
                "statements cannot have breakpoints."
            )
            self._view.show_message(msg, info)

    def rename_tab(self, tab_id=None):
        """
        How to handle double-clicking a tab in order to rename the file. If
        activated by the shortcut, activate against the current tab.
        """
        tab = None
        if tab_id:
            tab = self._view.tabs.widget(tab_id)
        else:
            tab = self._view.current_tab
        if tab:
            new_path = self._view.get_save_path(tab.path)
            if new_path and new_path != tab.path:
                if self.check_for_shadow_module(new_path):
                    message = _('You cannot use the filename "{}"').format(
                        os.path.basename(new_path)
                    )
                    info = _(
                        "This name is already used by another part of "
                        "Python. If you use that name, things are "
                        "likely to break. Please try again with a "
                        "different filename."
                    )
                    self._view.show_message(message, info)
                    return
                logger.info("Attempting to rename {} to {}".format(tab.path, new_path))
                # The user specified a path to a file.
                if not self.has_python_extension(os.path.basename(new_path)):
                    # No extension given, default to .py
                    new_path += ".py"
                # Check for duplicate path with currently open tab.
                for other_tab in self._view.widgets:
                    if other_tab.path == new_path:
                        logger.info(
                            "Cannot rename, a file of that name is already open in Nu"
                        )
                        message = _("Could not rename file.")
                        information = _("A file of that name is already open in Nu.")
                        self._view.show_message(message, information)
                        return
                # Finally rename
                tab.path = new_path
                logger.info("Renamed file to: {}".format(tab.path))
                self.save()

    def find_replace(self):
        """
        Handle find / replace functionality.

        If find/replace dialog is dismissed, do nothing.

        Otherwise, check there's something to find, warn if there isn't.

        If there is, find (and, optionally, replace) then confirm outcome with
        a status message.
        """
        result = self._view.show_find_replace(
            self.find, self.replace, self.global_replace
        )
        if result:
            self.find, self.replace, self.global_replace = result
            if self.find:
                if self.replace:
                    replaced = self._view.replace_text(
                        self.find, self.replace, self.global_replace
                    )
                    if replaced == 1:
                        msg = _('Replaced "{}" with "{}".')
                        self.show_status_message(msg.format(self.find, self.replace))
                    elif replaced > 1:
                        msg = _('Replaced {} matches of "{}" with "{}".')
                        self.show_status_message(
                            msg.format(replaced, self.find, self.replace)
                        )
                    else:
                        msg = _('Could not find "{}".')
                        self.show_status_message(msg.format(self.find))
                else:
                    matched = self._view.highlight_text(self.find)
                    if matched:
                        msg = _('Highlighting matches for "{}".')
                    else:
                        msg = _('Could not find "{}".')
                    self.show_status_message(msg.format(self.find))
            else:
                message = _("You must provide something to find.")
                information = _(
                    "Please try again, this time with something in the find box."
                )
                self._view.show_message(message, information)

    def find_again(self, forward=True):
        """
        Handle find again (F3 and Shift+F3) functionality.
        """
        if self.find:
            matched = self._view.highlight_text(self.find, forward)
            if matched:
                msg = _('Highlighting matches for "{}".')
            else:
                msg = _('Could not find "{}".')
            self.show_status_message(msg.format(self.find))
        else:
            message = _("You must provide something to find.")
            information = _(
                "Please try again, this time with something in the find box."
            )
            self._view.show_message(message, information)

    def find_again_backward(self, forward=False):
        """
        Handle find again backward (Shift+F3) functionality.
        """
        self.find_again(forward=False)

    def toggle_comments(self):
        """
        Ensure all highlighted lines are toggled between comments/uncommented.
        """
        self._view.toggle_comments()

    def tidy_code(self):
        """
        Prettify code with Black.
        """
        tab = self._view.current_tab
        if not tab or sys.version_info[:2] < (3, 6):
            return
        # Only works on Python, so abort.
        if tab.path and not self.has_python_extension(tab.path):
            return
        from black import format_str, FileMode, TargetVersion

        try:
            source_code = tab.text()
            logger.info("Tidy code.")
            logger.info(source_code)
            filemode = FileMode(
                target_versions={TargetVersion.PY36},
                line_length=MAX_LINE_LENGTH,
            )
            tidy_code = format_str(source_code, mode=filemode)
            # The following bypasses tab.setText which resets the undo history.
            # Doing it this way means the user can use CTRL-Z to undo the
            # reformatting from black.
            tab.SendScintilla(tab.SCI_SETTEXT, tidy_code.encode("utf-8"))
            self.show_status_message(
                _("Successfully cleaned the code. Use CTRL-Z to undo.")
            )
        except Exception as ex:
            # The user's code is problematic. Recover with a modal dialog
            # containing a helpful message.
            logger.error(ex)
            message = _("Your code contains problems.")
            information = _(
                "These must be fixed before tidying will work. "
                "Please use the 'Check' button to highlight "
                "these problems."
            )
            self._view.show_message(message, information)

    def has_python_extension(self, filename):
        """
        Check whether the given filename matches recognized Python extensions.
        """
        file_ends = filename.lower().endswith
        return any(file_ends(ext) for ext in self.python_extensions)

ask_to_change_mode(new_mode, mode_name, heading)

Open a dialog asking the user, whether to change mode from mode_name to new_mode. The dialog can be customized by the heading-parameter.

Source code in mu/logic.py
def ask_to_change_mode(self, new_mode, mode_name, heading):
    """
    Open a dialog asking the user, whether to change mode from
    mode_name to new_mode. The dialog can be customized by the
    heading-parameter.
    """
    # Only ask to switch mode if we're not already trying to
    # select a new mode via the dialog. Cannot change mode if
    # a script is already being run by the current mode.
    m = self.modes[self.mode]
    running = hasattr(m, "runner") and m.runner
    if (self.mode != new_mode and not self.selecting_mode) and not running:
        msg_body = _("Would you like to change Nu to the {} mode?").format(
            mode_name
        )
        change_confirmation = self._view.show_confirmation(
            heading, msg_body, icon="Question"
        )
        if change_confirmation == QMessageBox.StandardButton.Ok:
            self.change_mode(new_mode)

autosave()

Cycles through each tab and, if changed, saves it to the filesystem.

Source code in mu/logic.py
def autosave(self):
    """
    Cycles through each tab and, if changed, saves it to the filesystem.
    """
    if self._view.modified:
        # Something has changed, so save it!
        for tab in self._view.widgets:
            if tab.path and tab.isModified():
                # Suppress error message on autosave attempts
                self.save_tab_to_file(tab, show_error_messages=False)
                logger.info(
                    "Autosave detected and saved changes in {}.".format(tab.path)
                )

change_mode(mode)

Given the name of a mode, will make the necessary changes to put the editor into the new mode.

Source code in mu/logic.py
def change_mode(self, mode):
    """
    Given the name of a mode, will make the necessary changes to put the
    editor into the new mode.
    """
    # Remove the old mode's REPL / filesystem / plotter if required.
    old_mode = self.modes[self.mode]
    if hasattr(old_mode, "remove_repl"):
        old_mode.remove_repl()
    if hasattr(old_mode, "remove_fs"):
        old_mode.remove_fs()
    if hasattr(old_mode, "remove_plotter"):
        if old_mode.plotter:
            old_mode.remove_plotter()
    # Deactivate old mode
    self.modes[self.mode].deactivate()
    # Re-assign to new mode.
    self.mode = mode
    # Activate new mode
    self.modes[mode].activate()
    # Update buttons.
    self._view.change_mode(self.modes[mode])
    button_bar = self._view.button_bar
    button_bar.connect("modes", self.select_mode, "Ctrl+Shift+M")
    button_bar.connect("new", self.new, "Ctrl+N")
    button_bar.connect("load", self.load, "Ctrl+O")
    button_bar.connect("save", self.save, "Ctrl+S")
    for action in self.modes[mode].actions():
        button_bar.connect(action["name"], action["handler"], action["shortcut"])
    button_bar.connect("zoom-in", self.zoom_in, "Ctrl++")
    button_bar.connect("zoom-out", self.zoom_out, "Ctrl+-")
    button_bar.connect("theme", self.toggle_theme, "F1")
    button_bar.connect("check", self.check_code, "F2")
    if sys.version_info[:2] >= (3, 6):
        button_bar.connect("tidy", self.tidy_code, "F10")
    button_bar.connect("help", self.show_help, "Ctrl+H")
    button_bar.connect("quit", self.quit, "Ctrl+Q")
    self._view.status_bar.set_mode(self.modes[mode].name)
    # Update references to default file locations.
    try:
        workspace_dir = self.modes[mode].workspace_dir()
        logger.info("Workspace directory: {}".format(workspace_dir))
    except Exception as e:
        # Avoid crashing if workspace_dir raises, use default path instead
        workspace_dir = self.modes["python"].workspace_dir()
        logger.error(
            (
                "Could not open {} mode workspace directory, "
                'due to exception "{}".'
                "Using:\n\n{}\n\n...to store your code instead"
            ).format(mode, repr(e), workspace_dir)
        )
    # Reset remembered current path for load/save dialogs.
    self.current_path = ""
    # Ensure auto-save timeouts are set.
    if self.modes[mode].save_timeout > 0:
        # Start the timer
        self._view.set_timer(self.modes[mode].save_timeout, self.autosave)
    else:
        # Stop the timer
        self._view.stop_timer()
    # Update breakpoint states.
    if not (self.modes[mode].is_debugger or self.modes[mode].has_debugger):
        for tab in self._view.widgets:
            tab.breakpoint_handles = set()
            tab.reset_annotations()
    self.modes[mode].ensure_state()
    self.show_status_message(_("Changed to {} mode.").format(self.modes[mode].name))

check_code()

Uses PyFlakes and PyCodeStyle to gather information about potential problems with the code in the current tab.

Source code in mu/logic.py
def check_code(self):
    """
    Uses PyFlakes and PyCodeStyle to gather information about potential
    problems with the code in the current tab.
    """
    tab = self._view.current_tab
    if tab is None:
        # There is no active text editor so abort.
        return
    if tab.path and not self.has_python_extension(tab.path):
        # Only works on Python files, so abort.
        return
    tab.has_annotations = not tab.has_annotations
    if tab.has_annotations:
        logger.info("Checking code.")
        self._view.reset_annotations()
        filename = tab.path if tab.path else _("untitled")
        builtins = self.modes[self.mode].builtins
        flake = check_flake(filename, tab.text(), builtins)
        if flake:
            logger.info(flake)
            self._view.annotate_code(flake, "error")
        pep8 = check_pycodestyle(tab.text())
        if pep8:
            logger.info(pep8)
            self._view.annotate_code(pep8, "style")
        self._view.show_annotations()
        tab.has_annotations = bool(flake or pep8)
        if not tab.has_annotations:
            # No problems detected, so confirm this with a friendly
            # message.
            ok_messages = [
                _("Good job! No problems found."),
                _("Hurrah! Checker turned up no problems."),
                _("Nice one! Zero problems detected."),
                _("Well done! No problems here."),
                _("Awesome! Zero problems found."),
            ]
            self.show_status_message(random.choice(ok_messages))
            self._view.set_checker_icon("check-good")
        else:
            self._view.set_checker_icon("check-bad")
    else:
        self._view.reset_annotations()

check_for_shadow_module(path)

Check if the filename in the path is a shadow of a module already in the Python path. For example, many learners will save their first turtle based script as turtle.py, thus causing Python to never find the built in turtle module because of the name conflict.

If the filename shadows an existing module, return True, otherwise, return False.

Source code in mu/logic.py
def check_for_shadow_module(self, path):
    """
    Check if the filename in the path is a shadow of a module already in
    the Python path. For example, many learners will save their first
    turtle based script as turtle.py, thus causing Python to never find
    the built in turtle module because of the name conflict.

    If the filename shadows an existing module, return True, otherwise,
    return False.
    """
    logger.info('Checking path "{}" for shadow module.'.format(path))
    pyextensions = [".pyw", ".PYW", ".py", ".PY"]
    filename = os.path.basename(path)
    for ext in pyextensions:
        filename = filename.replace(ext, "")
    return filename in self.modes[self.mode].module_names

connect_to_status_bar(status_bar)

Connect the editor with the Window-statusbar. Should be called after Editor.setup(), to ensure modes are initialized

Source code in mu/logic.py
def connect_to_status_bar(self, status_bar):
    """
    Connect the editor with the Window-statusbar.
    Should be called after Editor.setup(), to ensure modes are initialized
    """
    # Connect to logs
    status_bar.connect_logs(self.show_admin, "Ctrl+Shift+D")
    # Show connection messages in status_bar
    self.connected_devices.device_connected.connect(status_bar.device_connected)
    # Connect to device list
    device_selector = status_bar.device_selector
    status_bar.device_selector.set_device_list(self.connected_devices)
    # Propagate device_changed events
    device_selector.device_changed.connect(self.device_changed)
    if self.modes:
        for mode in self.modes.values():
            device_selector.device_changed.connect(mode.device_changed)

debug_toggle_breakpoint(margin, line, modifiers)

How to handle the toggling of a breakpoint.

Source code in mu/logic.py
def debug_toggle_breakpoint(self, margin, line, modifiers):
    """
    How to handle the toggling of a breakpoint.
    """
    if self.modes[self.mode].has_debugger or self.modes[self.mode].is_debugger:
        tab = self._view.current_tab
        code = tab.text(line)
        if self.mode == "debugger":
            # The debugger is running.
            if is_breakpoint_line(code):
                self.modes["debugger"].toggle_breakpoint(line, tab)
                return
        else:
            # The debugger isn't running.
            if tab.markersAtLine(line):
                tab.markerDelete(line, -1)
                return
            elif is_breakpoint_line(code):
                handle = tab.markerAdd(line, tab.BREAKPOINT_MARKER)
                tab.breakpoint_handles.add(handle)
                return
        msg = _("Cannot Set Breakpoint.")
        info = _(
            "Lines that are comments or some multi-line "
            "statements cannot have breakpoints."
        )
        self._view.show_message(msg, info)

device_changed(device)

Slot for receiving signals that the current device has changed. If the device change requires mode change, the user will be asked through a dialog.

Source code in mu/logic.py
def device_changed(self, device):
    """
    Slot for receiving signals that the current device has changed.
    If the device change requires mode change, the user will be
    asked through a dialog.
    """
    if device:
        if self.current_device is None:
            heading = _("Detected new {} device.").format(device.name)
        else:
            heading = _("Device changed to {}.").format(device.name)
        self.ask_to_change_mode(
            device.short_mode_name, device.long_mode_name, heading
        )
    self.current_device = device

direct_load(path)

For loading files passed from command line or the OS launch.

Source code in mu/logic.py
def direct_load(self, path):
    """
    For loading files passed from command line or the OS launch.
    """
    self._load(path)

find_again(forward=True)

Handle find again (F3 and Shift+F3) functionality.

Source code in mu/logic.py
def find_again(self, forward=True):
    """
    Handle find again (F3 and Shift+F3) functionality.
    """
    if self.find:
        matched = self._view.highlight_text(self.find, forward)
        if matched:
            msg = _('Highlighting matches for "{}".')
        else:
            msg = _('Could not find "{}".')
        self.show_status_message(msg.format(self.find))
    else:
        message = _("You must provide something to find.")
        information = _(
            "Please try again, this time with something in the find box."
        )
        self._view.show_message(message, information)

find_again_backward(forward=False)

Handle find again backward (Shift+F3) functionality.

Source code in mu/logic.py
def find_again_backward(self, forward=False):
    """
    Handle find again backward (Shift+F3) functionality.
    """
    self.find_again(forward=False)

find_replace()

Handle find / replace functionality.

If find/replace dialog is dismissed, do nothing.

Otherwise, check there's something to find, warn if there isn't.

If there is, find (and, optionally, replace) then confirm outcome with a status message.

Source code in mu/logic.py
def find_replace(self):
    """
    Handle find / replace functionality.

    If find/replace dialog is dismissed, do nothing.

    Otherwise, check there's something to find, warn if there isn't.

    If there is, find (and, optionally, replace) then confirm outcome with
    a status message.
    """
    result = self._view.show_find_replace(
        self.find, self.replace, self.global_replace
    )
    if result:
        self.find, self.replace, self.global_replace = result
        if self.find:
            if self.replace:
                replaced = self._view.replace_text(
                    self.find, self.replace, self.global_replace
                )
                if replaced == 1:
                    msg = _('Replaced "{}" with "{}".')
                    self.show_status_message(msg.format(self.find, self.replace))
                elif replaced > 1:
                    msg = _('Replaced {} matches of "{}" with "{}".')
                    self.show_status_message(
                        msg.format(replaced, self.find, self.replace)
                    )
                else:
                    msg = _('Could not find "{}".')
                    self.show_status_message(msg.format(self.find))
            else:
                matched = self._view.highlight_text(self.find)
                if matched:
                    msg = _('Highlighting matches for "{}".')
                else:
                    msg = _('Could not find "{}".')
                self.show_status_message(msg.format(self.find))
        else:
            message = _("You must provide something to find.")
            information = _(
                "Please try again, this time with something in the find box."
            )
            self._view.show_message(message, information)

get_dialog_directory(default=None)

Return the directory folder which a load/save dialog box should open into. In order of precedence this function will return:

0) If not None, the value of default. 1) The last location used by a load/save dialog. 2) The directory containing the current file. 3) The mode's reported workspace directory.

Source code in mu/logic.py
def get_dialog_directory(self, default=None):
    """
    Return the directory folder which a load/save dialog box should
    open into. In order of precedence this function will return:

    0) If not None, the value of default.
    1) The last location used by a load/save dialog.
    2) The directory containing the current file.
    3) The mode's reported workspace directory.
    """
    if default is not None:
        folder = default
    elif self.current_path and os.path.isdir(self.current_path):
        folder = self.current_path
    else:
        current_file_path = ""
        try:
            workspace_path = self.modes[self.mode].workspace_dir()
        except Exception as e:
            # Avoid crashing if workspace_dir raises, use default path
            # instead
            workspace_path = self.modes["python"].workspace_dir()
            logger.error(
                (
                    "Could not open {} mode workspace directory"
                    'due to exception "{}". Using:'
                    "\n\n{}\n\n...to store your code instead"
                ).format(self.mode, e, workspace_path)
            )
        tab = self._view.current_tab
        if tab and tab.path:
            current_file_path = os.path.dirname(os.path.abspath(tab.path))
        folder = current_file_path if current_file_path else workspace_path
    logger.info("Using path for file dialog: {}".format(folder))
    return folder

get_tab(path)

Given a path, returns either an existing tab for the path or creates / loads a new tab for the path.

Source code in mu/logic.py
def get_tab(self, path):
    """
    Given a path, returns either an existing tab for the path or creates /
    loads a new tab for the path.
    """
    normalised_path = os.path.normcase(os.path.abspath(path))
    for tab in self._view.widgets:
        if tab.path:
            tab_path = os.path.normcase(os.path.abspath(tab.path))
            if tab_path == normalised_path:
                self._view.focus_tab(tab)
                return tab
    self.direct_load(path)
    return self._view.current_tab

has_python_extension(filename)

Check whether the given filename matches recognized Python extensions.

Source code in mu/logic.py
def has_python_extension(self, filename):
    """
    Check whether the given filename matches recognized Python extensions.
    """
    file_ends = filename.lower().endswith
    return any(file_ends(ext) for ext in self.python_extensions)

load(*args, default_path=None)

Loads a Python (or other supported) file from the file system or extracts a Python script from a hex file.

Source code in mu/logic.py
def load(self, *args, default_path=None):
    """
    Loads a Python (or other supported) file from the file system or
    extracts a Python script from a hex file.
    """
    # Get all supported extensions from the different modes
    extensions = [ext.strip("*.") for ext in self.python_extensions]
    for mode_name, mode in self.modes.items():
        if mode.file_extensions:
            extensions += mode.file_extensions
    extensions = [e.lower() for e in extensions]
    extensions = "*.{} *.{}".format(
        " *.".join(extensions), " *.".join(extensions).upper()
    )
    folder = self.get_dialog_directory(default_path)
    allow_previous = not bool(default_path)
    path = self._view.get_load_path(
        folder, extensions, allow_previous=allow_previous
    )
    if path:
        self.current_path = os.path.dirname(os.path.abspath(path))
        self._load(path)

load_cli(paths)

Given a set of paths, passed in by the user when Mu starts, this method will attempt to load them and log / report a problem if Mu is unable to open a passed in path.

Source code in mu/logic.py
def load_cli(self, paths):
    """
    Given a set of paths, passed in by the user when Mu starts, this
    method will attempt to load them and log / report a problem if Mu is
    unable to open a passed in path.
    """
    for p in paths:
        try:
            logger.info("Passed-in filename: {}".format(p))
            # abspath will fail for non-paths
            self.direct_load(os.path.abspath(p))
        except Exception as e:
            logging.warning(
                "Can't open file from command line {}".format(p),
                exc_info=e,
            )

new()

Adds a new tab to the editor.

Source code in mu/logic.py
def new(self):
    """
    Adds a new tab to the editor.
    """
    logger.info("Added a new tab.")
    default_text = self.modes[self.mode].code_template + NEWLINE
    self._view.add_tab(None, default_text, self.modes[self.mode].api(), NEWLINE)

quit(*args, **kwargs)

Exit the application.

Source code in mu/logic.py
def quit(self, *args, **kwargs):
    """
    Exit the application.
    """
    if self._view.modified:
        # Alert the user to handle unsaved work.
        msg = _(
            "There is un-saved work, exiting the application will"
            " cause you to lose it."
        )
        result = self._view.show_confirmation(msg)
        if result == QMessageBox.StandardButton.Cancel:
            if args and hasattr(args[0], "ignore"):
                # The function is handling an event, so ignore it.
                args[0].ignore()
            return
    paths = []
    for widget in self._view.widgets:
        if widget.path:
            paths.append(os.path.abspath(widget.path))
    # Make sure the mode's stop method is called so
    # everything is cleaned up.
    self.modes[self.mode].stop()
    session = {
        "theme": self.theme,
        "mode": self.mode,
        "paths": paths,
        "envars": self.envars,
        "zoom_level": self._view.zoom_position,
        "window": {
            "x": self._view.x(),
            "y": self._view.y(),
            "w": self._view.width(),
            "h": self._view.height(),
        },
        "locale": self.user_locale,
    }
    save_session(session)
    logger.info("Quitting.\n\n")
    QtCore.QCoreApplication.exit(0)

rename_tab(tab_id=None)

How to handle double-clicking a tab in order to rename the file. If activated by the shortcut, activate against the current tab.

Source code in mu/logic.py
def rename_tab(self, tab_id=None):
    """
    How to handle double-clicking a tab in order to rename the file. If
    activated by the shortcut, activate against the current tab.
    """
    tab = None
    if tab_id:
        tab = self._view.tabs.widget(tab_id)
    else:
        tab = self._view.current_tab
    if tab:
        new_path = self._view.get_save_path(tab.path)
        if new_path and new_path != tab.path:
            if self.check_for_shadow_module(new_path):
                message = _('You cannot use the filename "{}"').format(
                    os.path.basename(new_path)
                )
                info = _(
                    "This name is already used by another part of "
                    "Python. If you use that name, things are "
                    "likely to break. Please try again with a "
                    "different filename."
                )
                self._view.show_message(message, info)
                return
            logger.info("Attempting to rename {} to {}".format(tab.path, new_path))
            # The user specified a path to a file.
            if not self.has_python_extension(os.path.basename(new_path)):
                # No extension given, default to .py
                new_path += ".py"
            # Check for duplicate path with currently open tab.
            for other_tab in self._view.widgets:
                if other_tab.path == new_path:
                    logger.info(
                        "Cannot rename, a file of that name is already open in Nu"
                    )
                    message = _("Could not rename file.")
                    information = _("A file of that name is already open in Nu.")
                    self._view.show_message(message, information)
                    return
            # Finally rename
            tab.path = new_path
            logger.info("Renamed file to: {}".format(tab.path))
            self.save()

restore_session(paths=None)

Attempts to recreate the tab state from the last time the editor was run. If paths contains a collection of additional paths specified by the user, they are also "restored" at the same time (duplicates will be ignored).

Source code in mu/logic.py
def restore_session(self, paths=None):
    """
    Attempts to recreate the tab state from the last time the editor was
    run. If paths contains a collection of additional paths specified by
    the user, they are also "restored" at the same time (duplicates will be
    ignored).
    """
    self.change_mode(self.mode)
    old_session = settings.session
    logger.debug(old_session)
    if "theme" in old_session:
        self.theme = old_session["theme"]
    self._view.set_theme(self.theme)
    if "mode" in old_session:
        old_mode = old_session["mode"]
        if old_mode in self.modes:
            self.mode = old_session["mode"]
        else:
            # Unknown mode (perhaps an old version?)
            self.select_mode(None)
    else:
        # So ask for the desired mode.
        self.select_mode(None)
    if "paths" in old_session:
        old_paths = self._abspath(old_session["paths"])
        launch_paths = self._abspath(paths) if paths else set()
        for old_path in old_paths:
            # if the os passed in a file, defer loading it now
            if old_path in launch_paths:
                continue
            self.direct_load(old_path)
        logger.info("Loaded files.")
    if "envars" in old_session:
        old_envars = old_session["envars"]
        if isinstance(old_envars, list):
            old_envars = dict(old_envars)
        self.envars = old_envars
        logger.info("User defined environment variables: {}".format(self.envars))
    if "zoom_level" in old_session:
        self._view.zoom_position = old_session["zoom_level"]
        self._view.set_zoom()

    if "venv_path" in old_session:
        venv.relocate(old_session["venv_path"])
        venv.ensure()

    if "locale" in old_session:
        self.user_locale = old_session["locale"].strip()
        if self.user_locale:
            i18n.set_language(self.user_locale)

    old_window = old_session.get("window", {})
    self._view.size_window(**old_window)

    #
    # Doesn't seem to do anything useful
    #
    # ~ if old_session is None:
    # ~ self._view.set_theme(self.theme)

    # handle os passed file last,
    # so it will not be focused over by another tab
    if paths and len(paths) > 0:
        self.load_cli(paths)
    self.change_mode(self.mode)
    self.show_status_message(random.choice(MOTD), 10)
    if not self._view.tab_count:
        py = self.modes[self.mode].code_template + NEWLINE
        tab = self._view.add_tab(None, py, self.modes[self.mode].api(), NEWLINE)
        tab.setCursorPosition(len(py.split(NEWLINE)), 0)
        logger.info("Starting with blank file.")

save(*args, default=None)

Save the content of the currently active editor tab.

Source code in mu/logic.py
def save(self, *args, default=None):
    """
    Save the content of the currently active editor tab.
    """
    tab = self._view.current_tab
    if tab is None:
        # There is no active text editor so abort.
        return
    if not tab.path:
        # Unsaved file.
        folder = self.get_dialog_directory(default)
        path = self._view.get_save_path(folder)
        if path and self.check_for_shadow_module(path):
            message = _('You cannot use the filename "{}"').format(
                os.path.basename(path)
            )
            info = _(
                "This name is already used by another part of "
                "Python. If you use this name, things are "
                "likely to break. Please try again with a "
                "different filename."
            )
            self._view.show_message(message, info)
            return
        tab.path = path
    if tab.path:
        # The user specified a path to a file.
        self.save_tab_to_file(tab)
    else:
        # The user cancelled the filename selection.
        tab.path = None

save_tab_to_file(tab, show_error_messages=True)

Given a tab, will attempt to save the script in the tab to the path associated with the tab. If there's a problem this will be logged and reported and the tab status will continue to show as Modified.

Source code in mu/logic.py
def save_tab_to_file(self, tab, show_error_messages=True):
    """
    Given a tab, will attempt to save the script in the tab to the path
    associated with the tab. If there's a problem this will be logged and
    reported and the tab status will continue to show as Modified.
    """
    logger.info("Saving script to: {}".format(tab.path))
    logger.debug(tab.text())
    try:
        save_and_encode(tab.text(), tab.path, tab.newline)
    except OSError as e:
        logger.error(e)
        error_message = _("Could not save file (disk problem)")
        information = _(
            "Error saving file to disk. Ensure you have "
            "permission to write the file and "
            "sufficient disk space."
        )
    except UnicodeEncodeError:
        error_message = _("Could not save file (encoding problem)")
        logger.exception(error_message)
        information = _(
            "Unable to convert all the characters. If you "
            "have an encoding line at the top of the file, "
            "remove it and try again."
        )
    else:
        error_message = information = None
    if error_message and show_error_messages:
        self._view.show_message(error_message, information)
    else:
        tab.setModified(False)
        self.show_status_message(_("Saved file: {}").format(tab.path))

select_mode(event=None)

Select the mode that editor is supposed to be in.

Source code in mu/logic.py
def select_mode(self, event=None):
    """
    Select the mode that editor is supposed to be in.
    """
    if self.modes[self.mode].is_debugger:
        return
    logger.info("Showing available modes: {}".format(list(self.modes.keys())))
    self.selecting_mode = True  # Flag to stop auto-detection of modes.
    new_mode = self._view.select_mode(self.modes, self.mode)
    self.selecting_mode = False
    if new_mode and new_mode != self.mode:
        logger.info("New mode selected: {}".format(new_mode))
        self.change_mode(new_mode)

setup(modes)

Define the available modes and ensure there's a default working directory.

Source code in mu/logic.py
def setup(self, modes):
    """
    Define the available modes and ensure there's a default working
    directory.
    """
    self.modes = modes
    self.connected_devices.modes = modes
    logger.info("Available modes: {}".format(", ".join(self.modes.keys())))
    # Ensure there is a workspace directory.
    wd = self.modes["python"].workspace_dir()
    if not os.path.exists(wd):
        logger.debug("Creating directory: {}".format(wd))
        os.makedirs(wd)
    # Ensure PyGameZero assets are copied over.
    images_path = os.path.join(wd, "images")
    fonts_path = os.path.join(wd, "fonts")
    sounds_path = os.path.join(wd, "sounds")
    music_path = os.path.join(wd, "music")
    if not os.path.exists(images_path):
        logger.debug("Creating directory: {}".format(images_path))
        os.makedirs(images_path)
        for img in DEFAULT_IMAGES:
            shutil.copy(path(img, "pygamezero/"), os.path.join(images_path, img))
    if not os.path.exists(fonts_path):
        logger.debug("Creating directory: {}".format(fonts_path))
        os.makedirs(fonts_path)
    if not os.path.exists(sounds_path):
        logger.debug("Creating directory: {}".format(sounds_path))
        os.makedirs(sounds_path)
        for sfx in DEFAULT_SOUNDS:
            shutil.copy(path(sfx, "pygamezero/"), os.path.join(sounds_path, sfx))
    if not os.path.exists(music_path):
        logger.debug("Creating directory: {}".format(music_path))
        os.makedirs(music_path)
    # Ensure Web based assets are copied over.
    template_path = os.path.join(wd, "templates")
    static_path = os.path.join(wd, "static")
    if not os.path.exists(template_path):
        logger.debug("Creating directory: {}".format(template_path))
        shutil.copytree(path("templates", "web/"), template_path)
    if not os.path.exists(static_path):
        logger.debug("Creating directory: {}".format(static_path))
        shutil.copytree(path("static", "web/"), static_path)
        # Copy all the static directories.
    # Start the timer to poll every second for an attached or removed
    # USB device.
    self._view.set_usb_checker(1, self.connected_devices.check_usb)

show_admin(event=None)

Cause the editor's admin dialog to be displayed to the user.

Ensure any changes to the envars is updated.

Source code in mu/logic.py
def show_admin(self, event=None):
    """
    Cause the editor's admin dialog to be displayed to the user.

    Ensure any changes to the envars is updated.
    """
    logger.info("Showing admin with logs from {}".format(LOG_FILE))
    envars = "\n".join(
        ["{}={}".format(name, value) for name, value in self.envars.items()]
    )
    settings = {
        "envars": envars,
        "locale": self.user_locale,
    }
    baseline_packages, user_packages = venv.installed_packages()
    packages = user_packages
    with open(LOG_FILE, "r", encoding="utf8") as logfile:
        new_settings = self._view.show_admin(
            logfile.read(),
            settings,
            "\n".join(packages),
            self.modes[self.mode],
            self.connected_devices,
        )
    if new_settings:
        if "envars" in new_settings:
            self.envars = extract_envars(new_settings["envars"])
        if "packages" in new_settings:
            new_packages = [
                p for p in new_settings["packages"].lower().split("\n") if p.strip()
            ]
            old_packages = [p.lower() for p in user_packages]
            self.sync_package_state(old_packages, new_packages)
        if "locale" in new_settings:
            self.user_locale = new_settings["locale"]
        # Ensure mode UI is updated given settings.
        self.modes[self.mode].ensure_state()
    else:
        logger.info("No admin settings changed.")

show_help()

Display browser based help about Nu (docs/help.md on the docs site).

Source code in mu/logic.py
def show_help(self):
    """
    Display browser based help about Nu (docs/help.md on the docs site).
    """
    logger.info("Showing help at %r.", HELP_URL)
    webbrowser.open_new(HELP_URL)

show_status_message(message, duration=5)

Displays the referenced message for duration seconds.

Source code in mu/logic.py
def show_status_message(self, message, duration=5):
    """
    Displays the referenced message for duration seconds.
    """
    self._view.status_bar.set_message(message, duration * 1000)

sync_package_state(old_packages, new_packages)

Given the state of the old third party packages, compared to the new third party packages, ensure that pip uninstalls and installs the packages so the currently available third party packages reflects the new state.

Source code in mu/logic.py
def sync_package_state(self, old_packages, new_packages):
    """
    Given the state of the old third party packages, compared to the new
    third party packages, ensure that pip uninstalls and installs the
    packages so the currently available third party packages reflects the
    new state.
    """
    old = set(old_packages)
    new = set(new_packages)
    logger.info("Synchronize package states...")
    logger.info("Old: {}".format(old))
    logger.info("New: {}".format(new))
    to_remove = old.difference(new)
    to_add = new.difference(old)
    if to_remove or to_add:
        logger.info("To add: {}".format(to_add))
        logger.info("To remove: {}".format(to_remove))
        logger.info("Virtualenv: {}".format(VENV_DIR))
        self._view.sync_packages(to_remove, to_add)

tidy_code()

Prettify code with Black.

Source code in mu/logic.py
def tidy_code(self):
    """
    Prettify code with Black.
    """
    tab = self._view.current_tab
    if not tab or sys.version_info[:2] < (3, 6):
        return
    # Only works on Python, so abort.
    if tab.path and not self.has_python_extension(tab.path):
        return
    from black import format_str, FileMode, TargetVersion

    try:
        source_code = tab.text()
        logger.info("Tidy code.")
        logger.info(source_code)
        filemode = FileMode(
            target_versions={TargetVersion.PY36},
            line_length=MAX_LINE_LENGTH,
        )
        tidy_code = format_str(source_code, mode=filemode)
        # The following bypasses tab.setText which resets the undo history.
        # Doing it this way means the user can use CTRL-Z to undo the
        # reformatting from black.
        tab.SendScintilla(tab.SCI_SETTEXT, tidy_code.encode("utf-8"))
        self.show_status_message(
            _("Successfully cleaned the code. Use CTRL-Z to undo.")
        )
    except Exception as ex:
        # The user's code is problematic. Recover with a modal dialog
        # containing a helpful message.
        logger.error(ex)
        message = _("Your code contains problems.")
        information = _(
            "These must be fixed before tidying will work. "
            "Please use the 'Check' button to highlight "
            "these problems."
        )
        self._view.show_message(message, information)

toggle_comments()

Ensure all highlighted lines are toggled between comments/uncommented.

Source code in mu/logic.py
def toggle_comments(self):
    """
    Ensure all highlighted lines are toggled between comments/uncommented.
    """
    self._view.toggle_comments()

toggle_theme()

Switches between themes (night, day or high-contrast).

Source code in mu/logic.py
def toggle_theme(self):
    """
    Switches between themes (night, day or high-contrast).
    """
    if self.theme == "day":
        self.theme = "night"
    elif self.theme == "night":
        self.theme = "contrast"
    else:
        self.theme = "day"
    logger.info("Toggle theme to: {}".format(self.theme))
    self._view.set_theme(self.theme)

zoom_in()

Make the editor's text bigger

Source code in mu/logic.py
def zoom_in(self):
    """
    Make the editor's text bigger
    """
    logger.info("Zoom in")
    self._view.zoom_in()

zoom_out()

Make the editor's text smaller.

Source code in mu/logic.py
def zoom_out(self):
    """
    Make the editor's text smaller.
    """
    logger.info("Zoom out")
    self._view.zoom_out()

MuFlakeCodeReporter

The class instantiates a reporter that creates structured data about code quality for Mu. Used by the PyFlakes module.

Source code in mu/logic.py
class MuFlakeCodeReporter:
    """
    The class instantiates a reporter that creates structured data about
    code quality for Mu. Used by the PyFlakes module.
    """

    def __init__(self):
        """
        Set up the reporter object to be used to report PyFlake's results.
        """
        self.log = []

    def unexpectedError(self, filename, message):
        """
        Called if an unexpected error occurred while trying to process the file
        called filename. The message parameter contains a description of the
        problem.
        """
        self.log.append({"line_no": 0, "filename": filename, "message": str(message)})

    def syntaxError(self, filename, message, line_no, column, source):
        """
        Records a syntax error in the file called filename.

        The message argument contains an explanation of the syntax error,
        line_no indicates the line where the syntax error occurred, column
        indicates the column on which the error occurred and source is the
        source code containing the syntax error.
        """
        msg = _(
            "Syntax error. Python cannot understand this line. Check for "
            "missing characters!"
        )
        self.log.append(
            {
                "message": msg,
                "line_no": int(line_no) - 1,  # Zero based counting in Mu.
                "column": column - 1,
                "source": source,
            }
        )

    def flake(self, message):
        """
        PyFlakes found something wrong with the code.
        """
        matcher = FLAKE_REGEX.match(str(message))
        if matcher:
            line_no, col, msg = matcher.groups()
            self.log.append(
                {
                    "line_no": int(line_no) - 1,  # Zero based counting in Mu.
                    "column": int(col),
                    "message": msg,
                }
            )
        else:
            self.log.append({"line_no": 0, "column": 0, "message": str(message)})

__init__()

Set up the reporter object to be used to report PyFlake's results.

Source code in mu/logic.py
def __init__(self):
    """
    Set up the reporter object to be used to report PyFlake's results.
    """
    self.log = []

flake(message)

PyFlakes found something wrong with the code.

Source code in mu/logic.py
def flake(self, message):
    """
    PyFlakes found something wrong with the code.
    """
    matcher = FLAKE_REGEX.match(str(message))
    if matcher:
        line_no, col, msg = matcher.groups()
        self.log.append(
            {
                "line_no": int(line_no) - 1,  # Zero based counting in Mu.
                "column": int(col),
                "message": msg,
            }
        )
    else:
        self.log.append({"line_no": 0, "column": 0, "message": str(message)})

syntaxError(filename, message, line_no, column, source)

Records a syntax error in the file called filename.

The message argument contains an explanation of the syntax error, line_no indicates the line where the syntax error occurred, column indicates the column on which the error occurred and source is the source code containing the syntax error.

Source code in mu/logic.py
def syntaxError(self, filename, message, line_no, column, source):
    """
    Records a syntax error in the file called filename.

    The message argument contains an explanation of the syntax error,
    line_no indicates the line where the syntax error occurred, column
    indicates the column on which the error occurred and source is the
    source code containing the syntax error.
    """
    msg = _(
        "Syntax error. Python cannot understand this line. Check for "
        "missing characters!"
    )
    self.log.append(
        {
            "message": msg,
            "line_no": int(line_no) - 1,  # Zero based counting in Mu.
            "column": column - 1,
            "source": source,
        }
    )

unexpectedError(filename, message)

Called if an unexpected error occurred while trying to process the file called filename. The message parameter contains a description of the problem.

Source code in mu/logic.py
def unexpectedError(self, filename, message):
    """
    Called if an unexpected error occurred while trying to process the file
    called filename. The message parameter contains a description of the
    problem.
    """
    self.log.append({"line_no": 0, "filename": filename, "message": str(message)})

check_flake(filename, code, builtins=None)

Given a filename and some code to be checked, uses the PyFlakesmodule to return a dictionary describing issues of code quality per line. See:

https://github.com/PyCQA/pyflakes

If a list symbols is passed in as "builtins" these are assumed to be additional builtins available when run by Mu.

Source code in mu/logic.py
def check_flake(filename, code, builtins=None):
    """
    Given a filename and some code to be checked, uses the PyFlakesmodule to
    return a dictionary describing issues of code quality per line. See:

    https://github.com/PyCQA/pyflakes

    If a list symbols is passed in as "builtins" these are assumed to be
    additional builtins available when run by Mu.
    """
    import_all = "from microbit import *" in code
    if import_all:
        # Massage code so "from microbit import *" is expanded so the symbols
        # are known to flake.
        code = code.replace("from microbit import *", EXPANDED_IMPORT)
    reporter = MuFlakeCodeReporter()
    check(code, filename, reporter)
    if builtins:
        builtins_regex = re.compile(BUILTINS_REGEX.format("|".join(builtins)))
    feedback = {}
    for log in reporter.log:
        if import_all:
            # Guard to stop unwanted "microbit.* imported but unused" messages.
            message = log["message"]
            if EXPAND_FALSE_POSITIVE.match(message):
                continue
        if builtins:
            if builtins_regex.match(log["message"]):
                continue
        if log["line_no"] not in feedback:
            feedback[log["line_no"]] = []
        feedback[log["line_no"]].append(log)
    return feedback

check_pycodestyle(code, config_file=False)

Given some code, uses the PyCodeStyle module (was PEP8) to return a list of items describing issues of coding style. See:

https://pycodestyle.readthedocs.io/en/latest/intro.html

Source code in mu/logic.py
def check_pycodestyle(code, config_file=False):
    """
    Given some code, uses the PyCodeStyle module (was PEP8) to return a list
    of items describing issues of coding style. See:

    https://pycodestyle.readthedocs.io/en/latest/intro.html
    """
    # PyCodeStyle reads input from files, so make a temporary file containing
    # the code.
    code_fd, code_filename = tempfile.mkstemp()
    os.close(code_fd)
    save_and_encode(code, code_filename)
    # Configure which PEP8 rules to ignore.
    ignore = (
        "E121",
        "E123",
        "E126",
        "E226",
        "E203",
        "E302",
        "E305",
        "E24",
        "E704",
        "W291",
        "W292",
        "W293",
        "W391",
        "W503",
    )
    style = StyleGuide(
        parse_argv=False,
        config_file=config_file,
        max_line_length=MAX_LINE_LENGTH,
    )

    # StyleGuide() returns pycodestyle module's own ignore list. That list may
    # be a default list or a custom list provided by the user
    # merge the above ignore list with StyleGuide() returned list, then
    # remove duplicates with set(), convert back to tuple()
    ignore = style.options.ignore + ignore
    style.options.ignore = tuple(set(ignore))

    checker = Checker(code_filename, options=style.options)
    # Re-route stdout to a temporary buffer to be parsed below.
    temp_out = io.StringIO()
    sys.stdout = temp_out
    # Check the code.
    checker.check_all()
    # Put stdout back and read the content of the buffer. Remove the temporary
    # file created at the start.
    sys.stdout = sys.__stdout__
    temp_out.seek(0)
    results = temp_out.read()
    temp_out.close()
    os.remove(code_filename)
    # Parse the output from the tool into a dictionary of structured data.
    style_feedback = {}
    for result in results.split("\n"):
        matcher = STYLE_REGEX.match(result)
        if matcher:
            line_no, col, msg = matcher.groups()
            line_no = int(line_no) - 1
            code, description = msg.split(" ", 1)
            if code == "E303":
                description += _(" above this line")
            if line_no not in style_feedback:
                style_feedback[line_no] = []
            # Capitalise the 1st letter keeping the rest of the str unmodified
            if description:
                description = description[0].upper() + description[1:]
            style_feedback[line_no].append(
                {
                    "line_no": line_no,
                    "column": int(col) - 1,
                    "message": description,
                    "code": code,
                }
            )
    return style_feedback

extract_envars(raw)

Returns a list of environment variables given a string containing NAME=VALUE definitions on separate lines.

Source code in mu/logic.py
def extract_envars(raw):
    """
    Returns a list of environment variables given a string containing
    NAME=VALUE definitions on separate lines.
    """
    result = {}
    for line in raw.split("\n"):
        definition = line.split("=", 1)
        if len(definition) == 2:
            result[definition[0].strip()] = definition[1].strip()
    return result

read_and_decode(filepath)

Read the contents of a file,

Source code in mu/logic.py
def read_and_decode(filepath):
    """
    Read the contents of a file,
    """
    sniffed_encoding = sniff_encoding(filepath)
    #
    # If sniff_encoding has found enough clues to indicate an encoding,
    # use that. Otherwise try a series of defaults before giving up.
    #
    if sniffed_encoding:
        logger.debug("Detected encoding %s", sniffed_encoding)
        candidate_encodings = [sniffed_encoding]
    else:
        candidate_encodings = [ENCODING, locale.getpreferredencoding()]

    with open(filepath, "rb") as f:
        btext = f.read()
    for encoding in candidate_encodings:
        logger.debug("Trying to decode with %s", encoding)
        try:
            text = btext.decode(encoding)
            logger.info("Decoded with %s", encoding)
            break
        except (UnicodeDecodeError, LookupError):
            continue
    else:
        raise UnicodeDecodeError(encoding, btext, 0, 0, "Unable to decode")

    #
    # Sniff and convert newlines here so that, by the time
    # the text reaches the editor it is ready to use. Then
    # convert everything to the Mu internal newline character
    #
    newline = sniff_newline_convention(text)
    logger.debug("Detected newline %r", newline)
    text = re.sub("\r\n", NEWLINE, text)
    return text, newline

save_and_encode(text, filepath, newline=os.linesep)

Detect the presence of an encoding cookie and use that encoding; if none is present, do not add one and use the Mu default encoding. If the codec is invalid, log a warning and fall back to the default.

Source code in mu/logic.py
def save_and_encode(text, filepath, newline=os.linesep):
    """
    Detect the presence of an encoding cookie and use that encoding; if
    none is present, do not add one and use the Mu default encoding.
    If the codec is invalid, log a warning and fall back to the default.
    """
    match = ENCODING_COOKIE_RE.match(text)
    if match:
        encoding = match.group(1)
        try:
            codecs.lookup(encoding)
        except LookupError:
            logger.warning("Invalid codec in encoding cookie: %s", encoding)
            encoding = ENCODING
    else:
        encoding = ENCODING

    with open(filepath, "w", encoding=encoding, newline="") as f:
        text_to_write = (
            newline.join(line.rstrip(" ") for line in text.splitlines()) + newline
        )
        write_and_flush(f, text_to_write)

sniff_encoding(filepath)

Determine the encoding of a file:

  • If there is a BOM, return the appropriate encoding
  • If there is a PEP 263 encoding cookie, return the appropriate encoding
  • Otherwise return None for read_and_decode to attempt several defaults
Source code in mu/logic.py
def sniff_encoding(filepath):
    """Determine the encoding of a file:

    * If there is a BOM, return the appropriate encoding
    * If there is a PEP 263 encoding cookie, return the appropriate encoding
    * Otherwise return None for read_and_decode to attempt several defaults
    """
    boms = [
        (codecs.BOM_UTF8, "utf-8-sig"),
        (codecs.BOM_UTF16_BE, "utf-16"),
        (codecs.BOM_UTF16_LE, "utf-16"),
    ]
    #
    # Try for a BOM
    #
    with open(filepath, "rb") as f:
        line = f.readline()
    for bom, encoding in boms:
        if line.startswith(bom):
            return encoding

    #
    # Look for a PEP 263 encoding cookie
    #
    default_encoding = locale.getpreferredencoding()
    try:
        uline = line.decode(default_encoding)
    except UnicodeDecodeError:
        #
        # Can't even decode the line in order to match the cookie
        #
        pass
    else:
        match = ENCODING_COOKIE_RE.match(uline)
        if match:
            cookie_codec = match.group(1)
            try:
                codecs.lookup(cookie_codec)
            except LookupError:
                logger.warning(
                    "Encoding cookie has invalid codec name: {}".format(cookie_codec)
                )
            else:
                return cookie_codec

    #
    # Fall back to the locale default
    #
    return None

sniff_newline_convention(text)

Determine which line-ending convention predominates in the text.

Windows usually has U+000D U+000A Posix usually has U+000A But editors can produce either convention from either platform. And a file which has been copied and edited around might even have both!

Source code in mu/logic.py
def sniff_newline_convention(text):
    """Determine which line-ending convention predominates in the text.

    Windows usually has U+000D U+000A
    Posix usually has U+000A
    But editors can produce either convention from either platform. And
    a file which has been copied and edited around might even have both!
    """
    candidates = [
        ("\r\n", "\r\n"),
        # Match \n at the start of the string
        # or \n preceded by any character other than \r
        ("\n", "^\n|[^\r]\n"),
    ]
    #
    # If no lines are present, default to the platform newline
    # If there's a tie, use the platform default
    #
    conventions_found = [(0, 1, os.linesep)]
    for candidate, pattern in candidates:
        instances = re.findall(pattern, text)
        convention = (len(instances), candidate == os.linesep, candidate)
        conventions_found.append(convention)
    majority_convention = max(conventions_found)
    return majority_convention[-1]

write_and_flush(fileobj, content)

Write content to the fileobj then flush and fsync to ensure the data is, in fact, written.

This is especially necessary for USB-attached devices

Source code in mu/logic.py
def write_and_flush(fileobj, content):
    """
    Write content to the fileobj then flush and fsync to ensure the data is,
    in fact, written.

    This is especially necessary for USB-attached devices
    """
    fileobj.write(content)
    fileobj.flush()
    #
    # Theoretically this shouldn't work; fsync takes a file descriptor,
    # not a file object. However, there's obviously some under-the-cover
    # mechanism which converts one to the other (at least on Windows)
    #
    os.fsync(fileobj)

mu.debugger

The debugger consists of two parts:

  • Client - used by Mu to process messages from the process being debugged.
  • Runner - created in a new process to run the code to be debugged.

Messages are passed via inter-process communication.

mu.debugger.client

Code used by the Mu application to communicate with the process being debugged.

mu.debugger.client

A debug client for the Mu editor.

Copyright (c) 2015-2017 Nicholas H.Tollervey and others (see the AUTHORS file).

This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/.

Breakpoint

Represents a breakpoint, identified by a breakpoint number (bpnum). Users set breakpoints to stop the debugger at a certain line (potentially in a named function) in a file.

Source code in mu/debugger/client.py
class Breakpoint:
    """
    Represents a breakpoint, identified by a breakpoint number (bpnum). Users
    set breakpoints to stop the debugger at a certain line (potentially in a
    named function) in a file.
    """

    def __init__(
        self,
        bpnum,
        filename,
        line,
        enabled=True,
        temporary=False,
        funcname=None,
    ):
        self.bpnum = bpnum
        self.filename = filename
        self.line = line
        self.enabled = enabled
        self.temporary = temporary
        self.funcname = funcname

    def __str__(self):
        return "{}:{}".format(self.filename, self.line)

CommandBufferHandler

Bases: QObject

Represents the work to be done on a separate thread for connecting and processing incoming messages.

Emits signals to indicate when messages are receievd or the connection fails at appropriate moments during the lifetime of a debug session.

Source code in mu/debugger/client.py
class CommandBufferHandler(QObject):
    """
    Represents the work to be done on a separate thread for connecting and
    processing incoming messages.

    Emits signals to indicate when messages are receievd or the connection
    fails at appropriate moments during the lifetime of a debug session.
    """

    on_command = pyqtSignal(str)  #: Signal emitted when a command is received.
    on_fail = pyqtSignal(str)  #: Emitted when there was a connection failure.

    def __init__(self, debugger):
        """
        Receive the debugger object containing the configuration attributes and
        socket for inter-process communication with the debug runner.
        """
        super().__init__()
        self.debugger = debugger
        self.stopped = False

    def worker(self):
        """
        Buffer input from a socket, emit complete debugger commands as signals.
        """
        connected = False
        tries = 0
        connection_attempts = 50  # Translates to 10 seconds.
        pause_between_attempts = 0.2
        while not connected:
            try:
                self.debugger.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                self.debugger.socket.connect((self.debugger.host, self.debugger.port))
                connected = True
            except ConnectionRefusedError:
                # Allow up to connection_attempts attempts to connect.
                # The Raspberry Pi is quite slow, so Mu needs to give the
                # debug runner enough time to start up and start listening.
                tries += 1
                if tries >= connection_attempts:
                    self.on_fail.emit(
                        _(
                            "Connection timed out. Is your "
                            "machine slow or busy? Free up some "
                            "of the machine's resources and try "
                            "again."
                        )
                    )
                    return
                time.sleep(pause_between_attempts)
            except OSError:
                # This will catch address related errors. Especially on OSX
                # this is usually solved by adding "127.0.0.1 localhost" to
                # /etc/hosts.
                self.on_fail.emit(
                    _(
                        "Could not find localhost.\n"
                        "Ensure you have '127.0.0.1 localhost' in "
                        "your /etc/hosts file."
                    )
                )
                return
        # Getting here means the connection has been established, so handle all
        # incoming data from the debug runner process.
        remainder = b""
        while not self.stopped:
            new_buffer = None
            try:
                new_buffer = self.debugger.socket.recv(1024)
            except Exception:
                # Stop if there's any failure in receiving data from the
                # runner.
                self.stopped = True
            if new_buffer:
                if new_buffer.endswith(self.debugger.ETX):
                    terminator = self.debugger.ETX
                    pos = new_buffer.rfind(self.debugger.ETX)
                    full_buffer = remainder + new_buffer[:pos]
                else:
                    terminator = None
                    full_buffer = remainder + new_buffer

                commands = full_buffer.split(self.debugger.ETX)
                if terminator is None:
                    remainder = commands.pop()
                else:
                    remainder = b""
                for command in commands:
                    command = command.decode("utf-8")
                    logger.debug(command)
                    self.on_command.emit(command)
            else:
                # If recv() returns None, the socket is closed.
                logger.debug("Debug client closed.")
                break

__init__(debugger)

Receive the debugger object containing the configuration attributes and socket for inter-process communication with the debug runner.

Source code in mu/debugger/client.py
def __init__(self, debugger):
    """
    Receive the debugger object containing the configuration attributes and
    socket for inter-process communication with the debug runner.
    """
    super().__init__()
    self.debugger = debugger
    self.stopped = False

worker()

Buffer input from a socket, emit complete debugger commands as signals.

Source code in mu/debugger/client.py
def worker(self):
    """
    Buffer input from a socket, emit complete debugger commands as signals.
    """
    connected = False
    tries = 0
    connection_attempts = 50  # Translates to 10 seconds.
    pause_between_attempts = 0.2
    while not connected:
        try:
            self.debugger.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.debugger.socket.connect((self.debugger.host, self.debugger.port))
            connected = True
        except ConnectionRefusedError:
            # Allow up to connection_attempts attempts to connect.
            # The Raspberry Pi is quite slow, so Mu needs to give the
            # debug runner enough time to start up and start listening.
            tries += 1
            if tries >= connection_attempts:
                self.on_fail.emit(
                    _(
                        "Connection timed out. Is your "
                        "machine slow or busy? Free up some "
                        "of the machine's resources and try "
                        "again."
                    )
                )
                return
            time.sleep(pause_between_attempts)
        except OSError:
            # This will catch address related errors. Especially on OSX
            # this is usually solved by adding "127.0.0.1 localhost" to
            # /etc/hosts.
            self.on_fail.emit(
                _(
                    "Could not find localhost.\n"
                    "Ensure you have '127.0.0.1 localhost' in "
                    "your /etc/hosts file."
                )
            )
            return
    # Getting here means the connection has been established, so handle all
    # incoming data from the debug runner process.
    remainder = b""
    while not self.stopped:
        new_buffer = None
        try:
            new_buffer = self.debugger.socket.recv(1024)
        except Exception:
            # Stop if there's any failure in receiving data from the
            # runner.
            self.stopped = True
        if new_buffer:
            if new_buffer.endswith(self.debugger.ETX):
                terminator = self.debugger.ETX
                pos = new_buffer.rfind(self.debugger.ETX)
                full_buffer = remainder + new_buffer[:pos]
            else:
                terminator = None
                full_buffer = remainder + new_buffer

            commands = full_buffer.split(self.debugger.ETX)
            if terminator is None:
                remainder = commands.pop()
            else:
                remainder = b""
            for command in commands:
                command = command.decode("utf-8")
                logger.debug(command)
                self.on_command.emit(command)
        else:
            # If recv() returns None, the socket is closed.
            logger.debug("Debug client closed.")
            break

ConnectionNotBootstrapped

Bases: Exception

The connection to the runner hasn't been completed.

Source code in mu/debugger/client.py
class ConnectionNotBootstrapped(Exception):
    """
    The connection to the runner hasn't been completed.
    """

    pass

Debugger

Bases: QObject

Represents the networked debugger client.

Source code in mu/debugger/client.py
class Debugger(QObject):
    """
    Represents the networked debugger client.
    """

    ETX = b"\x03"  # End transmission token.

    def __init__(self, host, port, proc=None):
        """
        Instantiate given a host, port and process for the debug runner.
        """
        self.host = host
        self.port = port
        self.proc = proc
        self.view = None  # Set after instantiation.
        super().__init__()

    def start(self):
        """
        Start the debugger session.
        """
        self.listener_thread = QThread(self.view.view)
        self.command_handler = CommandBufferHandler(self)
        self.command_handler.moveToThread(self.listener_thread)
        self.command_handler.on_command.connect(self.on_command)
        self.command_handler.on_fail.connect(self.on_fail)
        self.listener_thread.started.connect(self.command_handler.worker)
        self.listener_thread.start()

    def on_command(self, command):
        """
        Handle a command emitted by the client thread.
        """
        event, data = json.loads(command)
        if hasattr(self, "on_{}".format(event)):
            getattr(self, "on_{}".format(event))(**data)

    def on_fail(self, message):
        """
        Handle if there's a connection failure with the debug runner.
        """
        logger.error(message)
        self.view.debug_on_fail(message)

    def stop(self):
        """
        Shut down the debugger session.
        """
        self.command_handler.stopped = True
        self.listener_thread.quit()
        self.listener_thread.wait()
        if self.proc is not None:
            self.output("quit")
        self.socket.shutdown(socket.SHUT_WR)
        if self.proc is not None:
            # Wait for the runner process to die.
            self.proc.wait()

    def output(self, event, **data):
        """
        Send a command to the debug runner.
        """
        try:
            dumped = json.dumps((event, data)).encode("utf-8")
            self.socket.sendall(dumped + Debugger.ETX)
        except OSError as e:
            logger.debug("Debugger client error.")
            logger.debug(e)
        except AttributeError as e:
            logger.debug("Debugger client not connected to runner.")
            logger.debug(e)

    def breakpoint(self, breakpoint):
        """
        Given a breakpoint number or (filename, line), return an object
        representing the referenced breakpoint.
        """
        try:
            if isinstance(breakpoint, tuple):
                filename, line = breakpoint
                filename = os.path.normcase(os.path.abspath(filename))
                return self.bp_index[filename][line]
            else:
                return self.bp_list[breakpoint]
        except KeyError:
            raise UnknownBreakpoint()

    def breakpoints(self, filename):
        """
        Return all the breakpoints associated with the referenced file.
        """
        normalised = os.path.normcase(os.path.abspath(filename))
        return self.bp_index.get(normalised, {})

    # Commands that can be passed to the debug runner.

    def create_breakpoint(self, filename, line, temporary=False):
        """
        Create a new, enabled breakpoint at the specified line of the given
        file.
        """
        self.output("break", filename=filename, line=line, temporary=temporary)

    def enable_breakpoint(self, breakpoint):
        """
        Enable an existing breakpoint.
        """
        self.output("enable", bpnum=breakpoint.bpnum)

    def disable_breakpoint(self, breakpoint):
        """
        Disable an existing breakpoint.
        """
        self.output("disable", bpnum=breakpoint.bpnum)

    def ignore_breakpoint(self, breakpoint, count):
        """
        Ignore an existing breakpoint for "count" iterations.

        (N.B. Use a count of 0 to restore the breakpoint.
        """
        self.output("ignore", bpnum=breakpoint.bpnum, count=count)

    def clear_breakpoint(self, breakpoint):
        """
        Clear an existing breakpoint.
        """
        self.output("clear", bpnum=breakpoint.bpnum)

    def do_run(self):
        """
        Run the debugger until the next breakpoint.
        """
        self.output("continue")

    def do_step(self):
        """
        Step through one stack frame.
        """
        self.output("step")

    def do_next(self):
        """
        Go to the next line in the current stack frame.
        """
        self.output("next")

    def do_return(self):
        """
        Return to the previous stack frame.
        """
        self.output("return")

    # Handlers for events raised by the debug runner. These generally follow
    # the pattern of updating state in the client object to reflect that of
    # the debug runner, then calling a method in the UI layer to update the
    # GUI to reflect the changed state.

    def on_bootstrap(self, breakpoints):
        """
        The runner has finished setting up.
        """
        self.bp_index = {}
        self.bp_list = list([True])  # Breakpoints count from 1
        for bp_data in breakpoints:
            self.on_breakpoint_create(**bp_data)
        self.view.debug_on_bootstrap()

    def on_breakpoint_create(self, **bp_data):
        """
        The runner has created a breakpoint.
        """
        bp = Breakpoint(**bp_data)
        filename = os.path.normcase(os.path.abspath(bp.filename))
        self.bp_index.setdefault(filename, {}).setdefault(bp.line, bp)
        self.bp_list.append(bp)
        if bp.enabled:
            self.view.debug_on_breakpoint_enable(bp)
        else:
            self.view.debug_on_breakpoint_disable(bp)

    def on_breakpoint_enable(self, bpnum):
        """
        The runner has enabled the breakpoint referenced by breakpoint number.
        """
        bp = self.bp_list[bpnum]
        bp.enabled = True
        self.view.debug_on_breakpoint_enable(bp)

    def on_breakpoint_disable(self, bpnum):
        """
        The runner has disabled a breakpoint referenced by breakpoint number.
        """
        bp = self.bp_list[bpnum]
        bp.enabled = False
        self.view.debug_on_breakpoint_disable(bp)

    def on_breakpoint_ignore(self, bpnum, count):
        """
        The runner will ignore the referenced breakpoint "count" iterations.
        """
        bp = self.bp_list[bpnum]
        bp.ignore = count
        self.view.debug_on_breakpoint_ignore(bp, count)

    def on_breakpoint_clear(self, bpnum):
        """
        The runner has cleared the referenced breakpoint.
        """
        bp = self.bp_list[bpnum]
        self.view.debug_on_breakpoint_clear(bp)

    def on_stack(self, stack):
        """
        The runner has sent an update to the stack.
        """
        self.stack = stack
        self.view.debug_on_stack(stack)

    def on_restart(self):
        """
        The runner has restarted.
        """
        self.view.debug_on_restart()

    def on_finished(self):
        """
        The debug runner has finished running the script to be debugged.
        """
        self.view.debug_on_finished()

    def on_call(self, args):
        """
        The runner has called a function with the specified arguments.
        """
        self.view.debug_on_call(args)

    def on_return(self, retval):
        """
        The runner has returned from a function with the specified return
        value.
        """
        self.view.debug_on_return(retval)

    def on_line(self, filename, line):
        """
        The runner has moved to the specified line in the referenced file.
        """
        self.view.debug_on_line(filename, line)

    def on_exception(self, name, value):
        """
        The runner has encountered a named exception with an associated value.
        """
        msg = "Exception encountered in user's code: {} - {}"
        logger.info(msg.format(name, value))
        self.view.debug_on_exception(name, value)

    def on_postmortem(self, *args, **kwargs):
        """
        The runner encountered a fatal error and has died.
        """
        self.view.debug_on_postmortem(args, kwargs)

    def on_info(self, message):
        """
        The runner has sent an informative message.
        """
        logger.info("Debug runner says: {}".format(message))
        self.view.debug_on_info(message)

    def on_warning(self, message):
        """
        The runner has sent a warning message.
        """
        logger.warning("Debug runner says: {}".format(message))
        self.view.debug_on_warning(message)

    def on_error(self, message):
        """
        The runner has sent an error message.
        """
        logger.error("Debug runner says: {}".format(message))
        self.view.debug_on_error(message)

__init__(host, port, proc=None)

Instantiate given a host, port and process for the debug runner.

Source code in mu/debugger/client.py
def __init__(self, host, port, proc=None):
    """
    Instantiate given a host, port and process for the debug runner.
    """
    self.host = host
    self.port = port
    self.proc = proc
    self.view = None  # Set after instantiation.
    super().__init__()

breakpoint(breakpoint)

Given a breakpoint number or (filename, line), return an object representing the referenced breakpoint.

Source code in mu/debugger/client.py
def breakpoint(self, breakpoint):
    """
    Given a breakpoint number or (filename, line), return an object
    representing the referenced breakpoint.
    """
    try:
        if isinstance(breakpoint, tuple):
            filename, line = breakpoint
            filename = os.path.normcase(os.path.abspath(filename))
            return self.bp_index[filename][line]
        else:
            return self.bp_list[breakpoint]
    except KeyError:
        raise UnknownBreakpoint()

breakpoints(filename)

Return all the breakpoints associated with the referenced file.

Source code in mu/debugger/client.py
def breakpoints(self, filename):
    """
    Return all the breakpoints associated with the referenced file.
    """
    normalised = os.path.normcase(os.path.abspath(filename))
    return self.bp_index.get(normalised, {})

clear_breakpoint(breakpoint)

Clear an existing breakpoint.

Source code in mu/debugger/client.py
def clear_breakpoint(self, breakpoint):
    """
    Clear an existing breakpoint.
    """
    self.output("clear", bpnum=breakpoint.bpnum)

create_breakpoint(filename, line, temporary=False)

Create a new, enabled breakpoint at the specified line of the given file.

Source code in mu/debugger/client.py
def create_breakpoint(self, filename, line, temporary=False):
    """
    Create a new, enabled breakpoint at the specified line of the given
    file.
    """
    self.output("break", filename=filename, line=line, temporary=temporary)

disable_breakpoint(breakpoint)

Disable an existing breakpoint.

Source code in mu/debugger/client.py
def disable_breakpoint(self, breakpoint):
    """
    Disable an existing breakpoint.
    """
    self.output("disable", bpnum=breakpoint.bpnum)

do_next()

Go to the next line in the current stack frame.

Source code in mu/debugger/client.py
def do_next(self):
    """
    Go to the next line in the current stack frame.
    """
    self.output("next")

do_return()

Return to the previous stack frame.

Source code in mu/debugger/client.py
def do_return(self):
    """
    Return to the previous stack frame.
    """
    self.output("return")

do_run()

Run the debugger until the next breakpoint.

Source code in mu/debugger/client.py
def do_run(self):
    """
    Run the debugger until the next breakpoint.
    """
    self.output("continue")

do_step()

Step through one stack frame.

Source code in mu/debugger/client.py
def do_step(self):
    """
    Step through one stack frame.
    """
    self.output("step")

enable_breakpoint(breakpoint)

Enable an existing breakpoint.

Source code in mu/debugger/client.py
def enable_breakpoint(self, breakpoint):
    """
    Enable an existing breakpoint.
    """
    self.output("enable", bpnum=breakpoint.bpnum)

ignore_breakpoint(breakpoint, count)

Ignore an existing breakpoint for "count" iterations.

(N.B. Use a count of 0 to restore the breakpoint.

Source code in mu/debugger/client.py
def ignore_breakpoint(self, breakpoint, count):
    """
    Ignore an existing breakpoint for "count" iterations.

    (N.B. Use a count of 0 to restore the breakpoint.
    """
    self.output("ignore", bpnum=breakpoint.bpnum, count=count)

on_bootstrap(breakpoints)

The runner has finished setting up.

Source code in mu/debugger/client.py
def on_bootstrap(self, breakpoints):
    """
    The runner has finished setting up.
    """
    self.bp_index = {}
    self.bp_list = list([True])  # Breakpoints count from 1
    for bp_data in breakpoints:
        self.on_breakpoint_create(**bp_data)
    self.view.debug_on_bootstrap()

on_breakpoint_clear(bpnum)

The runner has cleared the referenced breakpoint.

Source code in mu/debugger/client.py
def on_breakpoint_clear(self, bpnum):
    """
    The runner has cleared the referenced breakpoint.
    """
    bp = self.bp_list[bpnum]
    self.view.debug_on_breakpoint_clear(bp)

on_breakpoint_create(**bp_data)

The runner has created a breakpoint.

Source code in mu/debugger/client.py
def on_breakpoint_create(self, **bp_data):
    """
    The runner has created a breakpoint.
    """
    bp = Breakpoint(**bp_data)
    filename = os.path.normcase(os.path.abspath(bp.filename))
    self.bp_index.setdefault(filename, {}).setdefault(bp.line, bp)
    self.bp_list.append(bp)
    if bp.enabled:
        self.view.debug_on_breakpoint_enable(bp)
    else:
        self.view.debug_on_breakpoint_disable(bp)

on_breakpoint_disable(bpnum)

The runner has disabled a breakpoint referenced by breakpoint number.

Source code in mu/debugger/client.py
def on_breakpoint_disable(self, bpnum):
    """
    The runner has disabled a breakpoint referenced by breakpoint number.
    """
    bp = self.bp_list[bpnum]
    bp.enabled = False
    self.view.debug_on_breakpoint_disable(bp)

on_breakpoint_enable(bpnum)

The runner has enabled the breakpoint referenced by breakpoint number.

Source code in mu/debugger/client.py
def on_breakpoint_enable(self, bpnum):
    """
    The runner has enabled the breakpoint referenced by breakpoint number.
    """
    bp = self.bp_list[bpnum]
    bp.enabled = True
    self.view.debug_on_breakpoint_enable(bp)

on_breakpoint_ignore(bpnum, count)

The runner will ignore the referenced breakpoint "count" iterations.

Source code in mu/debugger/client.py
def on_breakpoint_ignore(self, bpnum, count):
    """
    The runner will ignore the referenced breakpoint "count" iterations.
    """
    bp = self.bp_list[bpnum]
    bp.ignore = count
    self.view.debug_on_breakpoint_ignore(bp, count)

on_call(args)

The runner has called a function with the specified arguments.

Source code in mu/debugger/client.py
def on_call(self, args):
    """
    The runner has called a function with the specified arguments.
    """
    self.view.debug_on_call(args)

on_command(command)

Handle a command emitted by the client thread.

Source code in mu/debugger/client.py
def on_command(self, command):
    """
    Handle a command emitted by the client thread.
    """
    event, data = json.loads(command)
    if hasattr(self, "on_{}".format(event)):
        getattr(self, "on_{}".format(event))(**data)

on_error(message)

The runner has sent an error message.

Source code in mu/debugger/client.py
def on_error(self, message):
    """
    The runner has sent an error message.
    """
    logger.error("Debug runner says: {}".format(message))
    self.view.debug_on_error(message)

on_exception(name, value)

The runner has encountered a named exception with an associated value.

Source code in mu/debugger/client.py
def on_exception(self, name, value):
    """
    The runner has encountered a named exception with an associated value.
    """
    msg = "Exception encountered in user's code: {} - {}"
    logger.info(msg.format(name, value))
    self.view.debug_on_exception(name, value)

on_fail(message)

Handle if there's a connection failure with the debug runner.

Source code in mu/debugger/client.py
def on_fail(self, message):
    """
    Handle if there's a connection failure with the debug runner.
    """
    logger.error(message)
    self.view.debug_on_fail(message)

on_finished()

The debug runner has finished running the script to be debugged.

Source code in mu/debugger/client.py
def on_finished(self):
    """
    The debug runner has finished running the script to be debugged.
    """
    self.view.debug_on_finished()

on_info(message)

The runner has sent an informative message.

Source code in mu/debugger/client.py
def on_info(self, message):
    """
    The runner has sent an informative message.
    """
    logger.info("Debug runner says: {}".format(message))
    self.view.debug_on_info(message)

on_line(filename, line)

The runner has moved to the specified line in the referenced file.

Source code in mu/debugger/client.py
def on_line(self, filename, line):
    """
    The runner has moved to the specified line in the referenced file.
    """
    self.view.debug_on_line(filename, line)

on_postmortem(*args, **kwargs)

The runner encountered a fatal error and has died.

Source code in mu/debugger/client.py
def on_postmortem(self, *args, **kwargs):
    """
    The runner encountered a fatal error and has died.
    """
    self.view.debug_on_postmortem(args, kwargs)

on_restart()

The runner has restarted.

Source code in mu/debugger/client.py
def on_restart(self):
    """
    The runner has restarted.
    """
    self.view.debug_on_restart()

on_return(retval)

The runner has returned from a function with the specified return value.

Source code in mu/debugger/client.py
def on_return(self, retval):
    """
    The runner has returned from a function with the specified return
    value.
    """
    self.view.debug_on_return(retval)

on_stack(stack)

The runner has sent an update to the stack.

Source code in mu/debugger/client.py
def on_stack(self, stack):
    """
    The runner has sent an update to the stack.
    """
    self.stack = stack
    self.view.debug_on_stack(stack)

on_warning(message)

The runner has sent a warning message.

Source code in mu/debugger/client.py
def on_warning(self, message):
    """
    The runner has sent a warning message.
    """
    logger.warning("Debug runner says: {}".format(message))
    self.view.debug_on_warning(message)

output(event, **data)

Send a command to the debug runner.

Source code in mu/debugger/client.py
def output(self, event, **data):
    """
    Send a command to the debug runner.
    """
    try:
        dumped = json.dumps((event, data)).encode("utf-8")
        self.socket.sendall(dumped + Debugger.ETX)
    except OSError as e:
        logger.debug("Debugger client error.")
        logger.debug(e)
    except AttributeError as e:
        logger.debug("Debugger client not connected to runner.")
        logger.debug(e)

start()

Start the debugger session.

Source code in mu/debugger/client.py
def start(self):
    """
    Start the debugger session.
    """
    self.listener_thread = QThread(self.view.view)
    self.command_handler = CommandBufferHandler(self)
    self.command_handler.moveToThread(self.listener_thread)
    self.command_handler.on_command.connect(self.on_command)
    self.command_handler.on_fail.connect(self.on_fail)
    self.listener_thread.started.connect(self.command_handler.worker)
    self.listener_thread.start()

stop()

Shut down the debugger session.

Source code in mu/debugger/client.py
def stop(self):
    """
    Shut down the debugger session.
    """
    self.command_handler.stopped = True
    self.listener_thread.quit()
    self.listener_thread.wait()
    if self.proc is not None:
        self.output("quit")
    self.socket.shutdown(socket.SHUT_WR)
    if self.proc is not None:
        # Wait for the runner process to die.
        self.proc.wait()

UnknownBreakpoint

Bases: Exception

The client encountered an unknown breakpoint.

Source code in mu/debugger/client.py
class UnknownBreakpoint(Exception):
    """
    The client encountered an unknown breakpoint.
    """

    pass

mu.debugger.runner

The runner code controls the debug process.

mu.debugger.runner

A debug runner for the Mu editor.

Copyright (c) 2015-2017 Nicholas H.Tollervey and others (see the AUTHORS file).

This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/.

ClientClose

Bases: Exception

Cause the debugger to wait for a new client to connect.

Source code in mu/debugger/runner.py
class ClientClose(Exception):
    """
    Cause the debugger to wait for a new client to connect.
    """

    pass

DebugState

Bases: Enum

Enumerates the three possible states of a debugging session.

Source code in mu/debugger/runner.py
class DebugState(Enum):
    """
    Enumerates the three possible states of a debugging session.
    """

    NOT_STARTED = 0
    STARTING = 1
    STARTED = 2

Debugger

Bases: Bdb

Instances of this class represent and drive the debugging process.

Source code in mu/debugger/runner.py
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
class Debugger(bdb.Bdb):
    """
    Instances of this class represent and drive the debugging process.
    """

    ETX = b"\x03"  # End transmission token.

    def __init__(self, socket, host, port, skip=None):
        super().__init__(skip=skip)
        self._run_state = DebugState.NOT_STARTED
        self.socket = socket
        self.host = host
        self.port = port
        self.mainpyfile = ""
        self.client = None
        self.command_thread = None
        self.commands = None
        self.quitting = None
        self.botframe = None
        self.stopframe = None
        # A flag to indicate that set_trace was called once, instead of
        # set_continue, in the absence of breakpoints at script start. The
        # flag indicates that continue means set_continue from now on.
        self.continue_flag = False

    def output(self, event, **data):
        """
        Dumps data related to a referenced event to the socket.
        """
        try:
            dumped = json.dumps((event, data)).encode("utf-8")
            logging.debug(dumped)
            self.client.sendall(dumped + Debugger.ETX)
        except OSError as e:
            logger.debug("Debugger client error.")
            logger.debug(e)
        except AttributeError as e:
            logger.debug("Debugger client not connected to runner.")
            logger.debug(e)

    def output_stack(self):
        """
        Dump the current stack.

        If this is a normal situation, the top two frames are BDB and the
        runner executing the program. If there is an exception, there are two
        further extra frames. All these frames can be ignored.
        """
        str_index = 0
        sl = len(self.stack)  # Bound check for stack length.
        if sl > 1 and self.stack[1][0].f_code.co_filename == "<string>":
            str_index = 2
        elif sl > 3 and self.stack[3][0].f_code.co_filename == "<string>":
            str_index = 4
        stack_data = []
        if str_index > 0:
            for frame, line_no in self.stack[str_index:]:
                frame_data = (
                    line_no,
                    {
                        "filename": frame.f_code.co_filename,
                        "locals": {k: repr(v) for k, v in frame.f_locals.items()},
                        "globals": {k: repr(v) for k, v in frame.f_globals.items()},
                        "builtins": {k: repr(v) for k, v in frame.f_builtins.items()},
                        "restricted": getattr(frame, "f_restricted", ""),
                        "lasti": repr(frame.f_lasti),
                        "exc_type": repr(getattr(frame, "f_exc_type", "")),
                        "exc_value": repr(getattr(frame, "f_exc_value", "")),
                        "exc_traceback": repr(getattr(frame, "f_exc_traceback", "")),
                        "current": frame is self.curframe,
                    },
                )
                stack_data.append(frame_data)
        self.output("stack", stack=stack_data)

    def reset(self):
        """
        Reset state.
        """
        self.line = None
        self.stack = []
        self.curindex = 0
        self.curframe = None

    def setup(self, frame, traceback):
        """
        Start state should be set correctly.
        """
        self.reset()
        self.stack, self.curindex = self.get_stack(frame, traceback)
        self.curframe = self.stack[self.curindex][0]

    def interact(self, frame, traceback):
        """
        Contains the loop processing interactions with the debugger.
        """
        self.setup(frame, traceback)
        self.output_stack()
        while True:
            try:
                command, args = self.commands.get(block=True)
                if hasattr(self, "do_{}".format(command)):
                    try:
                        resume = getattr(self, "do_{}".format(command))(**args)
                        if resume:
                            # Resume running the program after the interaction.
                            break
                    except (ClientClose, Restart):
                        raise
                    except Exception as ex:
                        msg = 'Unhandled error with command "{}": {}'.format(
                            command, ex
                        )
                        self.output("error", message=msg)
                else:
                    self.output("error", message="Unknown command: {}".format(command))
            except (OSError, AttributeError, ClientClose):
                # Connection problem; try listening for new connection.
                client, addr = self.socket.accept()
                self.client = client
                self.commands = Queue()
                self.command_thread = Thread(target=command_buffer, args=(self,))
                self.command_thread.daemon = True
                self.command_thread.start()
                self.output(
                    "bootstrap",
                    breakpoints=[
                        {
                            "bpnum": bp.number,
                            "filename": bp.file,
                            "line": bp.line,
                            "temporary": bp.temporary,
                            "enabled": bp.enabled,
                            "funcname": bp.funcname,
                        }
                        for bp in bdb.Breakpoint.bpbynumber[1:]
                    ],
                )
                self.output_stack()
        # End
        self.reset()

    # Overridden Bdb methods
    # See https://docs.python.org/3.6/library/bdb.html#bdb.Bdb.user_call

    def user_call(self, frame, argument_list):
        """
        This method is called from dispatch_call() when there is the
        possibility that a break might be necessary anywhere inside the called
        function.
        """
        if self._run_state == DebugState.STARTING:
            return
        if self.stop_here(frame):
            self.output("call", args=argument_list)
            self.interact(frame, None)

    def user_line(self, frame):
        """
        This method is called from dispatch_line() when either stop_here() or
        break_here() yields True.

        For when we stop or break at this line.
        """
        if self._run_state == DebugState.STARTING:
            self._run_state = DebugState.STARTED
        self.output(
            "line",
            filename=self.canonic(frame.f_code.co_filename),
            line=frame.f_lineno,
        )
        self.interact(frame, None)

    def user_return(self, frame, return_value):
        """
        This method is called from dispatch_return() when stop_here() yields
        True.

        For when a return trap is set here.
        """
        if self._run_state == DebugState.STARTING:
            return
        frame.f_locals["__return__"] = return_value
        self.output("return", retval=repr(return_value))
        self.interact(frame, None)

    def user_exception(self, frame, exc_info):
        """
        This method is called from dispatch_exception() when stop_here()
        yields True.

        For when an exception occurs, but only if we are to stop at or just
        below this level.
        """
        if self._run_state == DebugState.STARTING:
            return
        exc_type, exc_value, exc_traceback = exc_info
        frame.f_locals["__exception__"] = exc_type, exc_value
        if isinstance(exc_type, str):
            exc_type_name = exc_type
        else:
            exc_type_name = exc_type.__name__
        self.output("exception", name=exc_type_name, value=repr(exc_value))
        self.interact(frame, exc_traceback)

    # Debug command handlers.

    def do_break(self, filename, line, temporary=False):
        """
        Set a breakpoint.
        """
        globs = self.curframe.f_globals if hasattr(self, "curframe") else None
        code = linecache.getline(filename, line, globs)
        if is_breakpoint_line(code):
            err = self.set_break(filename, line, temporary, None, None)
            if err:
                self.output("error", message=err)
            else:
                bp = self.get_breaks(filename, line)[-1]
                self.output(
                    "breakpoint_create",
                    bpnum=bp.number,
                    filename=bp.file,
                    line=bp.line,
                    temporary=bp.temporary,
                    funcname=bp.funcname,
                )
        else:
            self.output(
                "error",
                message="{}:{} is not executable".format(filename, line),
            )

    def do_enable(self, bpnum):
        """
        Enables the breakpoint referenced by its breakpoint number (bpnum).
        """
        bpnum = int(bpnum)
        if not (0 <= bpnum < len(bdb.Breakpoint.bpbynumber)):
            self.output("error", message="No breakpoint numbered {}".format(bpnum))
        else:
            bp = bdb.Breakpoint.bpbynumber[bpnum]
            bp.enable()
            self.output("breakpoint_enable", bpnum=bpnum)

    def do_disable(self, bpnum):
        """
        Disable the breakpoint referenced by its breakpoint number (bpnum).
        """
        bpnum = int(bpnum)
        if not (0 <= bpnum < len(bdb.Breakpoint.bpbynumber)):
            self.output("error", message="No breakpoint numbered {}".format(bpnum))
        else:
            bp = bdb.Breakpoint.bpbynumber[bpnum]
            bp.disable()
            self.output("breakpoint_disable", bpnum=bpnum)

    def do_ignore(self, bpnum, count):
        """
        Ignore the breakpoint referenced by its breakpoint number (bpnum),
        count number of times.
        """
        try:
            count = int(count)
        except ValueError:
            count = 0
        if not (0 <= bpnum < len(bdb.Breakpoint.bpbynumber)):
            self.output("error", message="No breakpoint numbered {}".format(bpnum))
        else:
            bp = bdb.Breakpoint.bpbynumber[bpnum]
            bp.ignore = count
            if count > 0:
                self.output("breakpoint_ignore", bpnum=bpnum, count=count)
            else:
                self.output("breakpoint_enable", bpnum=bpnum)

    def do_clear(self, bpnum):
        """
        Handle how a breakpoint must be removed when it is a temporary one.
        """
        bpnum = int(bpnum)
        if not (0 <= bpnum < len(bdb.Breakpoint.bpbynumber)):
            self.output("error", message="No breakpoint numbered %s" % bpnum)
        else:
            err = self.clear_bpbynumber(bpnum)
            if err:
                self.output("error", message=err)
            else:
                self.output("breakpoint_clear", bpnum=bpnum)

    def do_step(self):
        """
        Stop after one line of code.
        """
        self.set_step()
        return True

    def do_next(self):
        """
        Stop on the next line in or below the given frame.
        """
        self.set_next(self.curframe)
        return True

    def do_restart(self):
        """
        Restart the program by raising an exception to be caught by the
        debugger.
        """
        raise Restart

    def do_return(self):
        """
        Stop when returning from the current frame.
        """
        self.set_return(self.curframe)
        return True

    def do_continue(self):
        """
        Stop only at breakpoints or when finished. If there are no breakpoints
        on script start, do a set_trace to stop at the first available line.
        However, use the continue_flag to ensure set_continue is always called
        thereafter.
        """
        if self.continue_flag or self.get_all_breaks():
            self.set_continue()
        else:
            self.set_step()
            self.continue_flag = True
        return True

    def do_quit(self):
        """
        Set the quitting attribute to True. This raises BdbQuit in the next
        call to one of the dispatch_*() methods.
        """
        self._user_requested_quit = True
        self.set_quit()
        return True

    def do_close(self):
        """
        Respond to a closed socket (not a user command, but needs handling).
        """
        self.client = None
        self.command_thread.join()
        self.commands = None
        raise ClientClose

    def _runscript(self, filename):
        """
        Start a debugging session with the Python script in the referenced
        filename.
        """
        # Clean __main__
        import __main__

        __main__.__dict__.clear()
        __main__.__dict__.update(
            {
                "__name__": "__main__",
                "__file__": filename,
                "__builtins__": __builtins__,
            }
        )
        # When bdb sets tracing, a number of call and line events happen BEFORE
        # we get to the user's code. The following measures avoid stopping
        # before we get to the target script (see user_line and user_call for
        # details).
        self._run_state = DebugState.STARTING
        self.mainpyfile = self.canonic(filename)
        self._user_requested_quit = True  # End the process once completed.
        e = (
            '__debug_script__ = open(r"{filename}", "rb");'
            "__debug_code__ = compile(__debug_script__.read(),"
            ' r"{filename}", "exec");'
            "exec(__debug_code__);"
            "__debug_script__.close();".format(filename=filename)
        )
        self.run(e)

do_break(filename, line, temporary=False)

Set a breakpoint.

Source code in mu/debugger/runner.py
def do_break(self, filename, line, temporary=False):
    """
    Set a breakpoint.
    """
    globs = self.curframe.f_globals if hasattr(self, "curframe") else None
    code = linecache.getline(filename, line, globs)
    if is_breakpoint_line(code):
        err = self.set_break(filename, line, temporary, None, None)
        if err:
            self.output("error", message=err)
        else:
            bp = self.get_breaks(filename, line)[-1]
            self.output(
                "breakpoint_create",
                bpnum=bp.number,
                filename=bp.file,
                line=bp.line,
                temporary=bp.temporary,
                funcname=bp.funcname,
            )
    else:
        self.output(
            "error",
            message="{}:{} is not executable".format(filename, line),
        )

do_clear(bpnum)

Handle how a breakpoint must be removed when it is a temporary one.

Source code in mu/debugger/runner.py
def do_clear(self, bpnum):
    """
    Handle how a breakpoint must be removed when it is a temporary one.
    """
    bpnum = int(bpnum)
    if not (0 <= bpnum < len(bdb.Breakpoint.bpbynumber)):
        self.output("error", message="No breakpoint numbered %s" % bpnum)
    else:
        err = self.clear_bpbynumber(bpnum)
        if err:
            self.output("error", message=err)
        else:
            self.output("breakpoint_clear", bpnum=bpnum)

do_close()

Respond to a closed socket (not a user command, but needs handling).

Source code in mu/debugger/runner.py
def do_close(self):
    """
    Respond to a closed socket (not a user command, but needs handling).
    """
    self.client = None
    self.command_thread.join()
    self.commands = None
    raise ClientClose

do_continue()

Stop only at breakpoints or when finished. If there are no breakpoints on script start, do a set_trace to stop at the first available line. However, use the continue_flag to ensure set_continue is always called thereafter.

Source code in mu/debugger/runner.py
def do_continue(self):
    """
    Stop only at breakpoints or when finished. If there are no breakpoints
    on script start, do a set_trace to stop at the first available line.
    However, use the continue_flag to ensure set_continue is always called
    thereafter.
    """
    if self.continue_flag or self.get_all_breaks():
        self.set_continue()
    else:
        self.set_step()
        self.continue_flag = True
    return True

do_disable(bpnum)

Disable the breakpoint referenced by its breakpoint number (bpnum).

Source code in mu/debugger/runner.py
def do_disable(self, bpnum):
    """
    Disable the breakpoint referenced by its breakpoint number (bpnum).
    """
    bpnum = int(bpnum)
    if not (0 <= bpnum < len(bdb.Breakpoint.bpbynumber)):
        self.output("error", message="No breakpoint numbered {}".format(bpnum))
    else:
        bp = bdb.Breakpoint.bpbynumber[bpnum]
        bp.disable()
        self.output("breakpoint_disable", bpnum=bpnum)

do_enable(bpnum)

Enables the breakpoint referenced by its breakpoint number (bpnum).

Source code in mu/debugger/runner.py
def do_enable(self, bpnum):
    """
    Enables the breakpoint referenced by its breakpoint number (bpnum).
    """
    bpnum = int(bpnum)
    if not (0 <= bpnum < len(bdb.Breakpoint.bpbynumber)):
        self.output("error", message="No breakpoint numbered {}".format(bpnum))
    else:
        bp = bdb.Breakpoint.bpbynumber[bpnum]
        bp.enable()
        self.output("breakpoint_enable", bpnum=bpnum)

do_ignore(bpnum, count)

Ignore the breakpoint referenced by its breakpoint number (bpnum), count number of times.

Source code in mu/debugger/runner.py
def do_ignore(self, bpnum, count):
    """
    Ignore the breakpoint referenced by its breakpoint number (bpnum),
    count number of times.
    """
    try:
        count = int(count)
    except ValueError:
        count = 0
    if not (0 <= bpnum < len(bdb.Breakpoint.bpbynumber)):
        self.output("error", message="No breakpoint numbered {}".format(bpnum))
    else:
        bp = bdb.Breakpoint.bpbynumber[bpnum]
        bp.ignore = count
        if count > 0:
            self.output("breakpoint_ignore", bpnum=bpnum, count=count)
        else:
            self.output("breakpoint_enable", bpnum=bpnum)

do_next()

Stop on the next line in or below the given frame.

Source code in mu/debugger/runner.py
def do_next(self):
    """
    Stop on the next line in or below the given frame.
    """
    self.set_next(self.curframe)
    return True

do_quit()

Set the quitting attribute to True. This raises BdbQuit in the next call to one of the dispatch_*() methods.

Source code in mu/debugger/runner.py
def do_quit(self):
    """
    Set the quitting attribute to True. This raises BdbQuit in the next
    call to one of the dispatch_*() methods.
    """
    self._user_requested_quit = True
    self.set_quit()
    return True

do_restart()

Restart the program by raising an exception to be caught by the debugger.

Source code in mu/debugger/runner.py
def do_restart(self):
    """
    Restart the program by raising an exception to be caught by the
    debugger.
    """
    raise Restart

do_return()

Stop when returning from the current frame.

Source code in mu/debugger/runner.py
def do_return(self):
    """
    Stop when returning from the current frame.
    """
    self.set_return(self.curframe)
    return True

do_step()

Stop after one line of code.

Source code in mu/debugger/runner.py
def do_step(self):
    """
    Stop after one line of code.
    """
    self.set_step()
    return True

interact(frame, traceback)

Contains the loop processing interactions with the debugger.

Source code in mu/debugger/runner.py
def interact(self, frame, traceback):
    """
    Contains the loop processing interactions with the debugger.
    """
    self.setup(frame, traceback)
    self.output_stack()
    while True:
        try:
            command, args = self.commands.get(block=True)
            if hasattr(self, "do_{}".format(command)):
                try:
                    resume = getattr(self, "do_{}".format(command))(**args)
                    if resume:
                        # Resume running the program after the interaction.
                        break
                except (ClientClose, Restart):
                    raise
                except Exception as ex:
                    msg = 'Unhandled error with command "{}": {}'.format(
                        command, ex
                    )
                    self.output("error", message=msg)
            else:
                self.output("error", message="Unknown command: {}".format(command))
        except (OSError, AttributeError, ClientClose):
            # Connection problem; try listening for new connection.
            client, addr = self.socket.accept()
            self.client = client
            self.commands = Queue()
            self.command_thread = Thread(target=command_buffer, args=(self,))
            self.command_thread.daemon = True
            self.command_thread.start()
            self.output(
                "bootstrap",
                breakpoints=[
                    {
                        "bpnum": bp.number,
                        "filename": bp.file,
                        "line": bp.line,
                        "temporary": bp.temporary,
                        "enabled": bp.enabled,
                        "funcname": bp.funcname,
                    }
                    for bp in bdb.Breakpoint.bpbynumber[1:]
                ],
            )
            self.output_stack()
    # End
    self.reset()

output(event, **data)

Dumps data related to a referenced event to the socket.

Source code in mu/debugger/runner.py
def output(self, event, **data):
    """
    Dumps data related to a referenced event to the socket.
    """
    try:
        dumped = json.dumps((event, data)).encode("utf-8")
        logging.debug(dumped)
        self.client.sendall(dumped + Debugger.ETX)
    except OSError as e:
        logger.debug("Debugger client error.")
        logger.debug(e)
    except AttributeError as e:
        logger.debug("Debugger client not connected to runner.")
        logger.debug(e)

output_stack()

Dump the current stack.

If this is a normal situation, the top two frames are BDB and the runner executing the program. If there is an exception, there are two further extra frames. All these frames can be ignored.

Source code in mu/debugger/runner.py
def output_stack(self):
    """
    Dump the current stack.

    If this is a normal situation, the top two frames are BDB and the
    runner executing the program. If there is an exception, there are two
    further extra frames. All these frames can be ignored.
    """
    str_index = 0
    sl = len(self.stack)  # Bound check for stack length.
    if sl > 1 and self.stack[1][0].f_code.co_filename == "<string>":
        str_index = 2
    elif sl > 3 and self.stack[3][0].f_code.co_filename == "<string>":
        str_index = 4
    stack_data = []
    if str_index > 0:
        for frame, line_no in self.stack[str_index:]:
            frame_data = (
                line_no,
                {
                    "filename": frame.f_code.co_filename,
                    "locals": {k: repr(v) for k, v in frame.f_locals.items()},
                    "globals": {k: repr(v) for k, v in frame.f_globals.items()},
                    "builtins": {k: repr(v) for k, v in frame.f_builtins.items()},
                    "restricted": getattr(frame, "f_restricted", ""),
                    "lasti": repr(frame.f_lasti),
                    "exc_type": repr(getattr(frame, "f_exc_type", "")),
                    "exc_value": repr(getattr(frame, "f_exc_value", "")),
                    "exc_traceback": repr(getattr(frame, "f_exc_traceback", "")),
                    "current": frame is self.curframe,
                },
            )
            stack_data.append(frame_data)
    self.output("stack", stack=stack_data)

reset()

Reset state.

Source code in mu/debugger/runner.py
def reset(self):
    """
    Reset state.
    """
    self.line = None
    self.stack = []
    self.curindex = 0
    self.curframe = None

setup(frame, traceback)

Start state should be set correctly.

Source code in mu/debugger/runner.py
def setup(self, frame, traceback):
    """
    Start state should be set correctly.
    """
    self.reset()
    self.stack, self.curindex = self.get_stack(frame, traceback)
    self.curframe = self.stack[self.curindex][0]

user_call(frame, argument_list)

This method is called from dispatch_call() when there is the possibility that a break might be necessary anywhere inside the called function.

Source code in mu/debugger/runner.py
def user_call(self, frame, argument_list):
    """
    This method is called from dispatch_call() when there is the
    possibility that a break might be necessary anywhere inside the called
    function.
    """
    if self._run_state == DebugState.STARTING:
        return
    if self.stop_here(frame):
        self.output("call", args=argument_list)
        self.interact(frame, None)

user_exception(frame, exc_info)

This method is called from dispatch_exception() when stop_here() yields True.

For when an exception occurs, but only if we are to stop at or just below this level.

Source code in mu/debugger/runner.py
def user_exception(self, frame, exc_info):
    """
    This method is called from dispatch_exception() when stop_here()
    yields True.

    For when an exception occurs, but only if we are to stop at or just
    below this level.
    """
    if self._run_state == DebugState.STARTING:
        return
    exc_type, exc_value, exc_traceback = exc_info
    frame.f_locals["__exception__"] = exc_type, exc_value
    if isinstance(exc_type, str):
        exc_type_name = exc_type
    else:
        exc_type_name = exc_type.__name__
    self.output("exception", name=exc_type_name, value=repr(exc_value))
    self.interact(frame, exc_traceback)

user_line(frame)

This method is called from dispatch_line() when either stop_here() or break_here() yields True.

For when we stop or break at this line.

Source code in mu/debugger/runner.py
def user_line(self, frame):
    """
    This method is called from dispatch_line() when either stop_here() or
    break_here() yields True.

    For when we stop or break at this line.
    """
    if self._run_state == DebugState.STARTING:
        self._run_state = DebugState.STARTED
    self.output(
        "line",
        filename=self.canonic(frame.f_code.co_filename),
        line=frame.f_lineno,
    )
    self.interact(frame, None)

user_return(frame, return_value)

This method is called from dispatch_return() when stop_here() yields True.

For when a return trap is set here.

Source code in mu/debugger/runner.py
def user_return(self, frame, return_value):
    """
    This method is called from dispatch_return() when stop_here() yields
    True.

    For when a return trap is set here.
    """
    if self._run_state == DebugState.STARTING:
        return
    frame.f_locals["__return__"] = return_value
    self.output("return", retval=repr(return_value))
    self.interact(frame, None)

Restart

Bases: Exception

Cause the debugger to restart for the target Python program.

Source code in mu/debugger/runner.py
class Restart(Exception):
    """
    Cause the debugger to restart for the target Python program.
    """

    pass

command_buffer(debugger)

Buffer input from a socket, yield complete debugger commands.

Source code in mu/debugger/runner.py
def command_buffer(debugger):
    """
    Buffer input from a socket, yield complete debugger commands.
    """
    remainder = b""
    while True:
        new_buffer = debugger.client.recv(1024)
        if new_buffer:
            if new_buffer.endswith(debugger.ETX):
                terminator = debugger.ETX
                pos = new_buffer.rfind(debugger.ETX)
                full_buffer = remainder + new_buffer[:pos]
            else:
                terminator = None
                full_buffer = remainder + new_buffer

            commands = full_buffer.split(debugger.ETX)
            if terminator is None:
                remainder = commands.pop()
            else:
                remainder = b""
            for command in commands:
                command = command.decode("utf-8")
                command_data = json.loads(command)
                logging.debug(command_data)
                debugger.commands.put(command_data)
        else:
            # If recv() returns None, the socket is closed.
            logger.debug("Closing debugger socket.")
            break
    debugger.commands.put(("close", {}))

run(hostname, port, filename, args)

Run a Python script identified by "filename" with the specified arguments in a debugger session that's listening at hostname/port.

Source code in mu/debugger/runner.py
def run(hostname, port, filename, args):
    """
    Run a Python script identified by "filename" with the specified arguments
    in a debugger session that's listening at hostname/port.
    """
    logger.debug("runner.run %s:%s %s %r", hostname, port, filename, args)
    # Create the correct context for the target Python script.
    sys.argv[0] = filename
    sys.argv[1:] = args
    sys.path[0] = os.path.dirname(filename)

    # Socket to which the client debugger connects.
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    s.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
    s.bind((hostname, port))
    s.listen(1)

    debugger = Debugger(s, hostname, port)
    debugger.reset()

    print(
        "Running in debug mode. Use the Stop, Continue, and Step toolbar"
        " buttons to debug the script",
        file=sys.stderr,
    )

    while True:
        try:
            debugger._runscript(filename)
            if debugger._user_requested_quit:
                debugger.output("finished")
                break
        except Restart:
            # TODO: Log restart.
            debugger.output("restart")
        except (KeyboardInterrupt, SystemExit, OSError):
            debugger.client = None
            break
        except Exception:
            msg = traceback.format_exc()
            debugger.output("postmortem", exception=msg)
            debugger.client = None
            break
    # Close connection in a tidy manner.
    if debugger.client:
        debugger.client.shutdown(socket.SHUT_WR)

mu.plugin

The public API for plugins. See Writing a Nu plugin.

mu.plugin.base

The plugin base classes and API versioning for Nu.

Copyright (c) 2026 the Nu contributors (see the AUTHORS file).

This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/.

ModePlugin

Bases: Plugin

A plugin whose only job is to contribute modes.

Set mode to a single Mode subclass, or modes to several, and they are registered for you.

Source code in mu/plugin/base.py
class ModePlugin(Plugin):
    """
    A plugin whose only job is to contribute modes.

    Set `mode` to a single Mode subclass, or `modes` to several, and they are
    registered for you.
    """

    #: A single Mode subclass to register.
    mode = None
    #: Several Mode subclasses to register (after `mode`, if both are set).
    modes = ()

    def register(self, host):
        mode_classes = ([self.mode] if self.mode else []) + list(self.modes)
        for mode_class in mode_classes:
            host.register_mode(mode_class)

Plugin

Base class for a Nu plugin.

Subclasses set name and api_version, then do all their wiring in register, using only the Host they are given.

Source code in mu/plugin/base.py
class Plugin:
    """
    Base class for a Nu plugin.

    Subclasses set `name` and `api_version`, then do all their wiring in
    `register`, using only the `Host` they are given.
    """

    #: Human readable name, used in logs and (later) the Plugins tab.
    name = "UNNAMED PLUGIN"
    #: The PLUGIN_API_VERSION this plugin was written against. Required.
    api_version = None

    def register(self, host):
        """
        Called once when the plugin is loaded. Use `host` to register modes
        and anything else the plugin contributes.
        """
        pass

    def unregister(self, host):
        """
        Called when the plugin is unloaded. Modes registered through `host`
        are removed automatically; undo anything else here.
        """
        pass

register(host)

Called once when the plugin is loaded. Use host to register modes and anything else the plugin contributes.

Source code in mu/plugin/base.py
def register(self, host):
    """
    Called once when the plugin is loaded. Use `host` to register modes
    and anything else the plugin contributes.
    """
    pass

unregister(host)

Called when the plugin is unloaded. Modes registered through host are removed automatically; undo anything else here.

Source code in mu/plugin/base.py
def unregister(self, host):
    """
    Called when the plugin is unloaded. Modes registered through `host`
    are removed automatically; undo anything else here.
    """
    pass

check_compatible(api_version, core_version=PLUGIN_API_VERSION)

Return None if a plugin targeting api_version can run on a core providing core_version, otherwise a short reason why it can't.

A plugin is compatible when it targets the same MAJOR version and a MINOR version no newer than the core's. While MAJOR is 0 the MINOR versions must match exactly, since 0.x releases are allowed to break the API.

Source code in mu/plugin/base.py
def check_compatible(api_version, core_version=PLUGIN_API_VERSION):
    """
    Return None if a plugin targeting `api_version` can run on a core
    providing `core_version`, otherwise a short reason why it can't.

    A plugin is compatible when it targets the same MAJOR version and a MINOR
    version no newer than the core's. While MAJOR is 0 the MINOR versions must
    match exactly, since 0.x releases are allowed to break the API.
    """
    try:
        major, minor = _parse_version(api_version)
    except ValueError:
        return "it declares no valid api_version (got {!r})".format(api_version)
    core_major, core_minor = _parse_version(core_version)
    if major != core_major or (major == 0 and minor != core_minor):
        return "it targets plugin API {}, but this Nu provides {}".format(
            api_version, core_version
        )
    if minor > core_minor:
        return "it needs plugin API {}, newer than this Nu's {}".format(
            api_version, core_version
        )
    return None

mu.plugin.host

The Host: the façade the core hands to plugins.

Copyright (c) 2026 the Nu contributors (see the AUTHORS file).

This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/.

Host

The only supported way for a plugin to reach the editor.

The core is free to change behind this class; a plugin should never need mu.logic, mu.interface or mu.modes internals. One Host is created per editor window and handed to every plugin's register/unregister.

Source code in mu/plugin/host.py
class Host:
    """
    The only supported way for a plugin to reach the editor.

    The core is free to change behind this class; a plugin should never need
    `mu.logic`, `mu.interface` or `mu.modes` internals. One Host is created per
    editor window and handed to every plugin's `register`/`unregister`.
    """

    #: The plugin API version this core provides.
    api_version = PLUGIN_API_VERSION

    def __init__(self, editor, view):
        self._editor = editor
        self._view = view
        self._modes = {}  # short_name -> mode instance
        self._owners = {}  # short_name -> owning plugin (None for the core)
        self._plugins = []
        self._loading = None  # the plugin whose register() is running
        #: What happened to each discovered plugin (mu.plugin.loader.PluginRecord),
        #: filled in at startup by app.setup_modes.
        self.plugin_records = []
        # Reports errors in plugin code so they don't crash Nu (see errors.py).
        self._error_handler = PluginErrorHandler(self)

    # --- plugin lifecycle (used by the core) ---

    @property
    def modes(self):
        """The registered modes, as a {short_name: mode} dict (a copy)."""
        return dict(self._modes)

    @property
    def plugins(self):
        """The successfully loaded plugins, in load order (a copy)."""
        return list(self._plugins)

    def load_plugin(self, plugin):
        """
        Check `plugin` is compatible and call its `register`. Return True if
        it loaded.

        This never raises: an incompatible plugin, or one whose `register`
        fails, is logged and skipped, and any modes it registered before
        failing are removed again.
        """
        name = getattr(plugin, "name", repr(plugin))
        reason = check_compatible(getattr(plugin, "api_version", None))
        if reason:
            logger.warning("Skipping plugin %s: %s", name, reason)
            return False
        self._loading = plugin
        try:
            plugin.register(self)
        except Exception:
            logger.exception("Plugin %s failed to register; skipping it", name)
            self._remove_modes_of(plugin)
            return False
        finally:
            self._loading = None
        self._plugins.append(plugin)
        logger.info("Loaded plugin %s", name)
        return True

    def unload_plugin(self, plugin):
        """
        Call `plugin.unregister` and remove the modes it registered. Errors in
        `unregister` are logged, not raised.
        """
        name = getattr(plugin, "name", repr(plugin))
        try:
            plugin.unregister(self)
        except Exception:
            logger.exception("Plugin %s failed to unregister", name)
        self._remove_modes_of(plugin)
        if plugin in self._plugins:
            self._plugins.remove(plugin)
        logger.info("Unloaded plugin %s", name)

    def _modes_of(self, plugin):
        """The mode instances `plugin` registered."""
        return [
            self._modes[name] for name, owner in self._owners.items() if owner is plugin
        ]

    def _remove_modes_of(self, plugin):
        for short_name, owner in list(self._owners.items()):
            if owner is plugin:
                mode = self._modes.pop(short_name)
                del self._owners[short_name]
                try:
                    mode.stop()
                except Exception:
                    logger.exception("Mode %s failed to stop", short_name)

    # --- registration (call from Plugin.register) ---

    def register_mode(self, mode_class):
        """
        Create an instance of `mode_class` (a `mu.plugin.Mode` subclass) and
        make it available in the editor. Its `short_name` must be unique.
        Returns the new mode instance, whose `host` attribute is this Host.
        """
        if not (isinstance(mode_class, type) and issubclass(mode_class, BaseMode)):
            raise TypeError("{!r} is not a Mode subclass".format(mode_class))
        short_name = mode_class.short_name
        if short_name in self._modes:
            raise ValueError(
                "A mode called {!r} is already registered".format(short_name)
            )
        mode = mode_class(self._editor, self._view)
        mode.host = self
        if self._loading is not None:
            # A plugin's mode: errors in the methods the core calls are
            # reported instead of crashing Nu.
            self._error_handler.guard_mode(mode, self._loading)
        self._modes[short_name] = mode
        self._owners[short_name] = self._loading
        return mode

    # --- talking to the learner ---

    def show_message(self, message, information=None, icon=None):
        """
        Show a modal message. `icon` is one of "NoIcon", "Question",
        "Information", "Warning" (the default) or "Critical".
        """
        self._view.show_message(message, information, icon)

    def confirm(self, message, information=None, icon=None):
        """
        Ask the learner to confirm with OK/Cancel; return True for OK. `icon`
        is as for `show_message`.
        """
        result = self._view.show_confirmation(message, information, icon)
        return result == QMessageBox.StandardButton.Ok

    def show_status(self, message, duration=5):
        """Show `message` in the status bar for `duration` seconds."""
        self._editor.show_status_message(message, duration)

    # --- the editor's state ---

    def current_text(self):
        """The text in the current tab, or None if no tab is open."""
        tab = self._view.current_tab
        return tab.text() if tab else None

    def current_path(self):
        """The file path of the current tab, or None if unsaved or no tab."""
        tab = self._view.current_tab
        return tab.path if tab else None

    def workspace_dir(self):
        """The learner's workspace directory (where files are saved)."""
        return BaseMode.workspace_dir()

    def open_directory(self, path):
        """Open `path` in the operating system's file manager."""
        self._view.open_directory_from_os(path)

    # --- housekeeping ---

    def settings(self, namespace):
        """Settings that persist between sessions, private to `namespace`."""
        return PluginSettings(namespace)

    def logger(self, name):
        """A logger whose output goes to Nu's log file, as "mu.plugins.<name>"."""
        return logging.getLogger("mu.plugins." + name)

modes property

The registered modes, as a {short_name: mode} dict (a copy).

plugins property

The successfully loaded plugins, in load order (a copy).

confirm(message, information=None, icon=None)

Ask the learner to confirm with OK/Cancel; return True for OK. icon is as for show_message.

Source code in mu/plugin/host.py
def confirm(self, message, information=None, icon=None):
    """
    Ask the learner to confirm with OK/Cancel; return True for OK. `icon`
    is as for `show_message`.
    """
    result = self._view.show_confirmation(message, information, icon)
    return result == QMessageBox.StandardButton.Ok

current_path()

The file path of the current tab, or None if unsaved or no tab.

Source code in mu/plugin/host.py
def current_path(self):
    """The file path of the current tab, or None if unsaved or no tab."""
    tab = self._view.current_tab
    return tab.path if tab else None

current_text()

The text in the current tab, or None if no tab is open.

Source code in mu/plugin/host.py
def current_text(self):
    """The text in the current tab, or None if no tab is open."""
    tab = self._view.current_tab
    return tab.text() if tab else None

load_plugin(plugin)

Check plugin is compatible and call its register. Return True if it loaded.

This never raises: an incompatible plugin, or one whose register fails, is logged and skipped, and any modes it registered before failing are removed again.

Source code in mu/plugin/host.py
def load_plugin(self, plugin):
    """
    Check `plugin` is compatible and call its `register`. Return True if
    it loaded.

    This never raises: an incompatible plugin, or one whose `register`
    fails, is logged and skipped, and any modes it registered before
    failing are removed again.
    """
    name = getattr(plugin, "name", repr(plugin))
    reason = check_compatible(getattr(plugin, "api_version", None))
    if reason:
        logger.warning("Skipping plugin %s: %s", name, reason)
        return False
    self._loading = plugin
    try:
        plugin.register(self)
    except Exception:
        logger.exception("Plugin %s failed to register; skipping it", name)
        self._remove_modes_of(plugin)
        return False
    finally:
        self._loading = None
    self._plugins.append(plugin)
    logger.info("Loaded plugin %s", name)
    return True

logger(name)

A logger whose output goes to Nu's log file, as "mu.plugins.".

Source code in mu/plugin/host.py
def logger(self, name):
    """A logger whose output goes to Nu's log file, as "mu.plugins.<name>"."""
    return logging.getLogger("mu.plugins." + name)

open_directory(path)

Open path in the operating system's file manager.

Source code in mu/plugin/host.py
def open_directory(self, path):
    """Open `path` in the operating system's file manager."""
    self._view.open_directory_from_os(path)

register_mode(mode_class)

Create an instance of mode_class (a mu.plugin.Mode subclass) and make it available in the editor. Its short_name must be unique. Returns the new mode instance, whose host attribute is this Host.

Source code in mu/plugin/host.py
def register_mode(self, mode_class):
    """
    Create an instance of `mode_class` (a `mu.plugin.Mode` subclass) and
    make it available in the editor. Its `short_name` must be unique.
    Returns the new mode instance, whose `host` attribute is this Host.
    """
    if not (isinstance(mode_class, type) and issubclass(mode_class, BaseMode)):
        raise TypeError("{!r} is not a Mode subclass".format(mode_class))
    short_name = mode_class.short_name
    if short_name in self._modes:
        raise ValueError(
            "A mode called {!r} is already registered".format(short_name)
        )
    mode = mode_class(self._editor, self._view)
    mode.host = self
    if self._loading is not None:
        # A plugin's mode: errors in the methods the core calls are
        # reported instead of crashing Nu.
        self._error_handler.guard_mode(mode, self._loading)
    self._modes[short_name] = mode
    self._owners[short_name] = self._loading
    return mode

settings(namespace)

Settings that persist between sessions, private to namespace.

Source code in mu/plugin/host.py
def settings(self, namespace):
    """Settings that persist between sessions, private to `namespace`."""
    return PluginSettings(namespace)

show_message(message, information=None, icon=None)

Show a modal message. icon is one of "NoIcon", "Question", "Information", "Warning" (the default) or "Critical".

Source code in mu/plugin/host.py
def show_message(self, message, information=None, icon=None):
    """
    Show a modal message. `icon` is one of "NoIcon", "Question",
    "Information", "Warning" (the default) or "Critical".
    """
    self._view.show_message(message, information, icon)

show_status(message, duration=5)

Show message in the status bar for duration seconds.

Source code in mu/plugin/host.py
def show_status(self, message, duration=5):
    """Show `message` in the status bar for `duration` seconds."""
    self._editor.show_status_message(message, duration)

unload_plugin(plugin)

Call plugin.unregister and remove the modes it registered. Errors in unregister are logged, not raised.

Source code in mu/plugin/host.py
def unload_plugin(self, plugin):
    """
    Call `plugin.unregister` and remove the modes it registered. Errors in
    `unregister` are logged, not raised.
    """
    name = getattr(plugin, "name", repr(plugin))
    try:
        plugin.unregister(self)
    except Exception:
        logger.exception("Plugin %s failed to unregister", name)
    self._remove_modes_of(plugin)
    if plugin in self._plugins:
        self._plugins.remove(plugin)
    logger.info("Unloaded plugin %s", name)

workspace_dir()

The learner's workspace directory (where files are saved).

Source code in mu/plugin/host.py
def workspace_dir(self):
    """The learner's workspace directory (where files are saved)."""
    return BaseMode.workspace_dir()

PluginSettings

A plugin's own corner of the user settings, persisted with them.

Values live under settings["plugins"][namespace], so plugins can't clash with each other or with the core's keys. Values must be JSON-serialisable.

Source code in mu/plugin/host.py
class PluginSettings:
    """
    A plugin's own corner of the user settings, persisted with them.

    Values live under `settings["plugins"][namespace]`, so plugins can't clash
    with each other or with the core's keys. Values must be JSON-serialisable.
    """

    def __init__(self, namespace, store=None):
        self.namespace = namespace
        self._store = settings.settings if store is None else store

    def _values(self):
        return dict(self._store.get("plugins", {}).get(self.namespace, {}))

    def get(self, key, default=None):
        """Return the value stored under `key`, or `default`."""
        return self._values().get(key, default)

    def set(self, key, value):
        """Store `value` under `key`; it is saved with the user's settings."""
        # Rebuild the nested dicts rather than mutating them in place, so the
        # settings object marks "plugins" as changed and saves it.
        values = self._values()
        values[key] = value
        plugins = dict(self._store.get("plugins", {}))
        plugins[self.namespace] = values
        self._store["plugins"] = plugins

    def __contains__(self, key):
        return key in self._values()

get(key, default=None)

Return the value stored under key, or default.

Source code in mu/plugin/host.py
def get(self, key, default=None):
    """Return the value stored under `key`, or `default`."""
    return self._values().get(key, default)

set(key, value)

Store value under key; it is saved with the user's settings.

Source code in mu/plugin/host.py
def set(self, key, value):
    """Store `value` under `key`; it is saved with the user's settings."""
    # Rebuild the nested dicts rather than mutating them in place, so the
    # settings object marks "plugins" as changed and saves it.
    values = self._values()
    values[key] = value
    plugins = dict(self._store.get("plugins", {}))
    plugins[self.namespace] = values
    self._store["plugins"] = plugins

mu.plugin.loader

Finding and loading plugins: installed packages and the drop-in folder.

Copyright (c) 2026 the Nu contributors (see the AUTHORS file).

This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/.

PluginRecord dataclass

What happened to one discovered plugin.

Source code in mu/plugin/loader.py
@dataclass
class PluginRecord:
    """What happened to one discovered plugin."""

    id: str
    kind: str
    origin: str
    status: str  # LOADED, DISABLED or SKIPPED
    reason: str = ""
    plugin: object = None

PluginSource dataclass

A plugin that has been found but not yet imported.

id is the stable name used to enable/disable it: the entry point name, or the module name in the drop-in folder. origin says where it came from for logs and the (future) Plugins tab. Calling load() imports it and returns a Plugin instance, or raises.

Source code in mu/plugin/loader.py
@dataclass
class PluginSource:
    """
    A plugin that has been found but not yet imported.

    `id` is the stable name used to enable/disable it: the entry point name,
    or the module name in the drop-in folder. `origin` says where it came from
    for logs and the (future) Plugins tab. Calling `load()` imports it and
    returns a Plugin instance, or raises.
    """

    id: str
    kind: str  # "package" or "folder"
    origin: str
    load: object = field(repr=False)

disabled_plugins(store=None)

The ids of the plugins the user has switched off.

Source code in mu/plugin/loader.py
def disabled_plugins(store=None):
    """The ids of the plugins the user has switched off."""
    store = settings.settings if store is None else store
    return set(store.get(DISABLED_SETTING, []))

discover(dropin_dir=None)

Every plugin Nu can see: installed packages first, then the drop-in folder. Nothing is imported yet.

Source code in mu/plugin/loader.py
def discover(dropin_dir=None):
    """
    Every plugin Nu can see: installed packages first, then the drop-in
    folder. Nothing is imported yet.
    """
    try:
        sources = entry_point_sources()
    except Exception:
        logger.exception("Can't read installed plugin entry points")
        sources = []
    return sources + dropin_sources(dropin_dir)

dropin_sources(dirpath=None)

Plugins dropped into dirpath: each name.py file or name/ package (with an __init__.py) is one plugin, with id name. The folder is created if it doesn't exist, so it's there for learners to find. Defaults to DROPIN_DIR.

Source code in mu/plugin/loader.py
def dropin_sources(dirpath=None):
    """
    Plugins dropped into `dirpath`: each `name.py` file or `name/` package
    (with an `__init__.py`) is one plugin, with id `name`. The folder is
    created if it doesn't exist, so it's there for learners to find.
    Defaults to DROPIN_DIR.
    """
    if dirpath is None:
        dirpath = DROPIN_DIR
    try:
        os.makedirs(dirpath, exist_ok=True)
        entries = sorted(os.listdir(dirpath))
    except OSError:
        logger.exception("Can't read the plugins folder %s", dirpath)
        return []
    sources = []
    for entry in entries:
        path = os.path.join(dirpath, entry)
        if entry.startswith((".", "_")):
            continue
        if os.path.isdir(path):
            if not os.path.isfile(os.path.join(path, "__init__.py")):
                continue
            name = entry
        elif entry.endswith(".py"):
            name = entry[:-3]
        else:
            continue
        if not name.isidentifier():
            logger.warning("Ignoring %s: not a valid Python module name", path)
            continue

        def load(name=name, path=path):
            module = _import_dropin(name, path)
            return _instantiate(_dropin_plugin_object(module), name)

        sources.append(PluginSource(name, "folder", path, load))
    return sources

entry_point_sources(group=ENTRY_POINT_GROUP)

Plugins installed as packages in the environment Nu runs from, found through their mu.plugins entry points.

Source code in mu/plugin/loader.py
def entry_point_sources(group=ENTRY_POINT_GROUP):
    """
    Plugins installed as packages in the environment Nu runs from, found
    through their `mu.plugins` entry points.
    """
    sources = []
    for entry_point in importlib.metadata.entry_points(group=group):
        dist = entry_point.dist
        origin = "{} {}".format(dist.name, dist.version) if dist else entry_point.value

        def load(entry_point=entry_point):
            return _instantiate(entry_point.load(), entry_point.value)

        sources.append(PluginSource(entry_point.name, "package", origin, load))
    return sources

load_plugins(host, sources=None, store=None)

Load every discovered, enabled plugin into host and return a PluginRecord for each one found.

This never raises. A plugin that is switched off, shares an id with one already found, fails to import, targets an incompatible API version or fails in register is recorded (and logged) as skipped, and Nu carries on without it.

Source code in mu/plugin/loader.py
def load_plugins(host, sources=None, store=None):
    """
    Load every discovered, enabled plugin into `host` and return a
    PluginRecord for each one found.

    This never raises. A plugin that is switched off, shares an id with one
    already found, fails to import, targets an incompatible API version or
    fails in `register` is recorded (and logged) as skipped, and Nu carries
    on without it.
    """
    if sources is None:
        sources = discover()
    disabled = disabled_plugins(store)
    records = []
    seen = set()
    for source in sources:
        record = PluginRecord(source.id, source.kind, source.origin, SKIPPED)
        records.append(record)
        if source.id in seen:
            record.reason = "another plugin is already called {!r}".format(source.id)
            logger.warning("Skipping plugin %s (%s): %s", *_describe(record))
            continue
        seen.add(source.id)
        if source.id in disabled:
            record.status = DISABLED
            logger.info("Plugin %s (%s) is switched off", source.id, source.origin)
            continue
        try:
            plugin = source.load()
        except KeyboardInterrupt:
            raise
        except BaseException as ex:  # incl. SystemExit from a plugin's import
            record.reason = "it failed to import: {}".format(ex)
            logger.exception("Skipping plugin %s (%s)", source.id, source.origin)
            continue
        if plugin.name == Plugin.name:
            plugin.name = source.id  # unnamed: use its id in logs and the UI
        reason = check_compatible(getattr(plugin, "api_version", None))
        if reason:
            record.reason = reason
            logger.warning("Skipping plugin %s (%s): %s", *_describe(record))
            continue
        if host.load_plugin(plugin):
            record.status = LOADED
            record.plugin = plugin
        else:
            record.reason = "it failed to register (see the log)"
    return records

set_plugin_enabled(plugin_id, enabled, store=None)

Switch a plugin on or off; takes effect the next time Nu starts.

Source code in mu/plugin/loader.py
def set_plugin_enabled(plugin_id, enabled, store=None):
    """Switch a plugin on or off; takes effect the next time Nu starts."""
    store = settings.settings if store is None else store
    disabled = disabled_plugins(store)
    if enabled:
        disabled.discard(plugin_id)
    else:
        disabled.add(plugin_id)
    store[DISABLED_SETTING] = sorted(disabled)

mu.plugin.errors

Keeping Nu running when a plugin has an error.

Copyright (c) 2026 the Nu contributors (see the AUTHORS file).

This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/.

A plugin's code runs in two ways, and both are covered:

  • The core calls a plugin mode's methods (activate, actions, api, ...). PluginErrorHandler.guard_mode wraps those methods: an error is reported and a safe fallback is returned, so e.g. a mode whose api() fails just has no autocomplete.
  • Qt calls the plugin's button handlers, timers and signal handlers. An error there reaches sys.excepthook; app.excepthook first asks handle_uncaught, which recognises plugin code in the traceback and reports the error instead of letting Nu exit.

Either way the learner sees a dialog naming the plugin (rate limited, so a failing timer can't bury them in dialogs), the details go to the log, and a plugin that keeps failing is offered to be switched off.

PluginError

Bases: Exception

Raise this from plugin code for an expected problem the learner can fix, e.g. "Plug in your micro:bit first". Nu shows message (and the optional information) as is, rather than reporting a bug in the plugin.

Source code in mu/plugin/errors.py
class PluginError(Exception):
    """
    Raise this from plugin code for an *expected* problem the learner can
    fix, e.g. "Plug in your micro:bit first". Nu shows `message` (and the
    optional `information`) as is, rather than reporting a bug in the plugin.
    """

    def __init__(self, message, information=None):
        super().__init__(message)
        self.message = message
        self.information = information

PluginErrorHandler

Reports errors in plugin code to the learner and the log, so Nu keeps running. There is one per Host.

Source code in mu/plugin/errors.py
class PluginErrorHandler:
    """
    Reports errors in plugin code to the learner and the log, so Nu keeps
    running. There is one per Host.
    """

    #: Minimum seconds between error dialogs for the same plugin.
    DIALOG_INTERVAL = 30
    #: After this many errors, offer to switch the plugin off.
    SWITCH_OFF_AFTER = 3

    def __init__(self, host, clock=time.monotonic):
        self._host = host
        self._clock = clock
        self._states = {}
        self._dialog_open = False

    def _state(self, plugin):
        return self._states.setdefault(id(plugin), _PluginState())

    def error_count(self, plugin):
        """How many unexpected errors `plugin` has had this session."""
        return self._state(plugin).errors

    # --- working out whose code failed ---

    def _plugin_modules(self):
        """Map each loaded plugin's top-level module name to the plugin."""
        modules = {}
        for plugin in self._host.plugins:
            classes = [type(plugin)] + [
                type(mode) for mode in self._host._modes_of(plugin)
            ]
            for cls in classes:
                top = cls.__module__.split(".")[0]
                if top not in ("mu", "builtins", "__main__"):
                    modules[top] = plugin
        return modules

    def owner(self, tb):
        """
        The plugin whose code appears in traceback `tb` (the innermost one,
        if several), or None if it's all core code. A frame counts as the
        plugin's if it's in the plugin's modules, or is a method called on
        one of the plugin's modes (e.g. inherited MicroPythonMode code).
        """
        modules = self._plugin_modules()
        modes = {
            id(mode): plugin
            for plugin in self._host.plugins
            for mode in self._host._modes_of(plugin)
        }
        found = None
        for frame, _ in traceback.walk_tb(tb):
            top = frame.f_globals.get("__name__", "").split(".")[0]
            plugin = modules.get(top)
            if plugin is None:
                plugin = modes.get(id(frame.f_locals.get("self")))
            if plugin is not None:
                found = plugin
        return found

    def handle_uncaught(self, exc_type, exc, tb):
        """
        Called from `app.excepthook`. If plugin code raised the exception,
        report it and return True (Nu carries on); otherwise return False.
        """
        if issubclass(exc_type, KeyboardInterrupt):
            return False
        try:
            plugin = self.owner(tb)
        except Exception:
            logger.exception("Couldn't work out where an error came from")
            return False
        if plugin is None:
            return False
        if exc is None:
            exc = exc_type()
        self.report(plugin, exc, tb)
        return True

    # --- guarding the calls the core makes into plugin modes ---

    def guard_mode(self, mode, plugin):
        """Wrap `mode`'s core-called methods so errors are reported."""
        for name, fallback in GUARDED_MODE_METHODS.items():
            method = getattr(mode, name, None)
            if callable(method):
                setattr(mode, name, self._guarded(plugin, method, name, fallback))

    def _guarded(self, plugin, method, name, fallback):
        @functools.wraps(method)
        def guarded(*args, **kwargs):
            try:
                return method(*args, **kwargs)
            except (Exception, SystemExit) as ex:
                self.report(plugin, ex, ex.__traceback__, where=name)
                return fallback() if fallback else None

        return guarded

    # --- telling the learner ---

    def report(self, plugin, exc, tb=None, where=None):
        """
        Tell the learner (and the log) that `plugin` raised `exc`. This never
        raises.
        """
        try:
            if isinstance(exc, PluginError):
                self._report_expected(plugin, exc)
            else:
                self._report_unexpected(plugin, exc, tb, where)
        except Exception:
            logger.exception("Failed to report an error in plugin %s", plugin)

    def _report_expected(self, plugin, exc):
        logger.info("Plugin %s: %s", plugin.name, exc.message)
        if self._dialog_open:
            self._host.show_status(exc.message)
            return
        self._show_dialog(
            self._host.show_message, exc.message, exc.information, "Information"
        )

    def _report_unexpected(self, plugin, exc, tb, where):
        state = self._state(plugin)
        state.errors += 1
        signature = (type(exc).__name__, str(exc), where)
        location = " in {}()".format(where) if where else ""
        if signature in state.seen:
            logger.error(
                "Plugin %s failed again%s: %s: %s (error #%d)",
                plugin.name,
                location,
                type(exc).__name__,
                exc,
                state.errors,
            )
        else:
            state.seen.add(signature)
            logger.error(
                "Plugin %s failed%s",
                plugin.name,
                location,
                exc_info=(type(exc), exc, tb or exc.__traceback__),
            )

        now = self._clock()
        recently = (
            state.last_dialog is not None
            and now - state.last_dialog < self.DIALOG_INTERVAL
        )
        if self._dialog_open or recently:
            self._host.show_status(
                _("The {} plugin had another problem (see the log).").format(
                    plugin.name
                )
            )
            return
        state.last_dialog = now

        plugin_id = self._plugin_id(plugin)
        if (
            state.errors >= self.SWITCH_OFF_AFTER
            and plugin_id
            and not state.offered_switch_off
        ):
            state.offered_switch_off = True
            message = _("The {} plugin keeps having problems.").format(plugin.name)
            information = _(
                "Nu is still running. Switch the plugin off? It won't be "
                "loaded the next time Nu starts. (You can switch it back on "
                "by removing it from disabled_plugins in settings.json.)"
            )
            if self._show_dialog(self._host.confirm, message, information, "Question"):
                loader.set_plugin_enabled(plugin_id, False)
                self._host.show_status(
                    _("The {} plugin is switched off from the next start.").format(
                        plugin.name
                    )
                )
            return

        message = _(
            "The {} plugin had a problem and stopped what it was doing."
        ).format(plugin.name)
        information = _(
            "Nu is still running, so you can carry on. If it keeps happening, "
            "tell the plugin's author:\n\n{}: {}\n\nThe full details are in "
            "Nu's log."
        ).format(type(exc).__name__, exc)
        self._show_dialog(self._host.show_message, message, information, "Warning")

    def _show_dialog(self, show, message, information, icon):
        # A modal dialog runs a nested event loop, so a plugin timer can fail
        # again while it's open; those errors go to the status bar instead.
        self._dialog_open = True
        try:
            return show(message, information, icon)
        finally:
            self._dialog_open = False

    def _plugin_id(self, plugin):
        for record in self._host.plugin_records:
            if record.plugin is plugin:
                return record.id
        return None

error_count(plugin)

How many unexpected errors plugin has had this session.

Source code in mu/plugin/errors.py
def error_count(self, plugin):
    """How many unexpected errors `plugin` has had this session."""
    return self._state(plugin).errors

guard_mode(mode, plugin)

Wrap mode's core-called methods so errors are reported.

Source code in mu/plugin/errors.py
def guard_mode(self, mode, plugin):
    """Wrap `mode`'s core-called methods so errors are reported."""
    for name, fallback in GUARDED_MODE_METHODS.items():
        method = getattr(mode, name, None)
        if callable(method):
            setattr(mode, name, self._guarded(plugin, method, name, fallback))

handle_uncaught(exc_type, exc, tb)

Called from app.excepthook. If plugin code raised the exception, report it and return True (Nu carries on); otherwise return False.

Source code in mu/plugin/errors.py
def handle_uncaught(self, exc_type, exc, tb):
    """
    Called from `app.excepthook`. If plugin code raised the exception,
    report it and return True (Nu carries on); otherwise return False.
    """
    if issubclass(exc_type, KeyboardInterrupt):
        return False
    try:
        plugin = self.owner(tb)
    except Exception:
        logger.exception("Couldn't work out where an error came from")
        return False
    if plugin is None:
        return False
    if exc is None:
        exc = exc_type()
    self.report(plugin, exc, tb)
    return True

owner(tb)

The plugin whose code appears in traceback tb (the innermost one, if several), or None if it's all core code. A frame counts as the plugin's if it's in the plugin's modules, or is a method called on one of the plugin's modes (e.g. inherited MicroPythonMode code).

Source code in mu/plugin/errors.py
def owner(self, tb):
    """
    The plugin whose code appears in traceback `tb` (the innermost one,
    if several), or None if it's all core code. A frame counts as the
    plugin's if it's in the plugin's modules, or is a method called on
    one of the plugin's modes (e.g. inherited MicroPythonMode code).
    """
    modules = self._plugin_modules()
    modes = {
        id(mode): plugin
        for plugin in self._host.plugins
        for mode in self._host._modes_of(plugin)
    }
    found = None
    for frame, _ in traceback.walk_tb(tb):
        top = frame.f_globals.get("__name__", "").split(".")[0]
        plugin = modules.get(top)
        if plugin is None:
            plugin = modes.get(id(frame.f_locals.get("self")))
        if plugin is not None:
            found = plugin
    return found

report(plugin, exc, tb=None, where=None)

Tell the learner (and the log) that plugin raised exc. This never raises.

Source code in mu/plugin/errors.py
def report(self, plugin, exc, tb=None, where=None):
    """
    Tell the learner (and the log) that `plugin` raised `exc`. This never
    raises.
    """
    try:
        if isinstance(exc, PluginError):
            self._report_expected(plugin, exc)
        else:
            self._report_unexpected(plugin, exc, tb, where)
    except Exception:
        logger.exception("Failed to report an error in plugin %s", plugin)

handle_uncaught(exc_type, exc, tb)

For app.excepthook: return True if the exception came from a plugin and has been reported, so Nu should carry on.

Source code in mu/plugin/errors.py
def handle_uncaught(exc_type, exc, tb):
    """
    For `app.excepthook`: return True if the exception came from a plugin
    and has been reported, so Nu should carry on.
    """
    if _active is None:
        return False
    try:
        return _active.handle_uncaught(exc_type, exc, tb)
    except Exception:
        logger.exception("Plugin error handling failed")
        return False

install(handler)

Make handler the one app.excepthook consults.

Source code in mu/plugin/errors.py
def install(handler):
    """Make `handler` the one `app.excepthook` consults."""
    global _active
    _active = handler

mu.interface

This module contains all the PyQt related code needed to create the user interface for Mu. All interaction with the user interface is done via the Window class in mu.interface.main.

All the other sub-modules contain different bespoke aspects of the user interface.

mu.interface.main

Contains the core user interface assets used by other parts of the application.

mu.interface.main

Contains the main Window definition for Mu's UI.

Copyright (c) 2015-2017 Nicholas H.Tollervey and others (see the AUTHORS file).

This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/.

ButtonBar

Bases: QToolBar

Represents the bar of buttons across the top of the editor and defines their behaviour.

Source code in mu/interface/main.py
class ButtonBar(QToolBar):
    """
    Represents the bar of buttons across the top of the editor and defines
    their behaviour.
    """

    def __init__(self, parent):
        super().__init__(parent)
        self.setMovable(False)
        self.setIconSize(QSize(64, 64))
        self.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextUnderIcon)
        self.setContextMenuPolicy(Qt.ContextMenuPolicy.PreventContextMenu)
        self.setObjectName("StandardToolBar")
        self.reset()

    def reset(self):
        """
        Resets the button states.
        """
        self.slots = {}
        self.clear()

    def change_mode(self, mode):
        self.reset()
        self.addAction(
            name="modes",
            display_name=_("Mode"),
            tool_text=_("Change Nu's mode of behaviour."),
        )
        self.addSeparator()
        self.addAction(
            name="new",
            display_name=_("New"),
            tool_text=_("Create a new Python script."),
        )
        self.addAction(
            name="load",
            display_name=_("Load"),
            tool_text=_("Load a Python script."),
        )
        self.addAction(
            name="save",
            display_name=_("Save"),
            tool_text=_("Save the current Python script."),
        )
        self.addSeparator()

        for action in mode.actions():
            self.addAction(
                name=action["name"],
                display_name=action["display_name"],
                tool_text=action["description"],
            )

        self.addSeparator()
        self.addAction(
            name="zoom-in",
            display_name=_("Zoom-in"),
            tool_text=_("Zoom in (to make the text bigger)."),
        )
        self.addAction(
            name="zoom-out",
            display_name=_("Zoom-out"),
            tool_text=_("Zoom out (to make the text smaller)."),
        )
        self.addAction(
            name="theme",
            display_name=_("Theme"),
            tool_text=_("Toggle theme between day, night or high contrast."),
        )
        self.addSeparator()
        self.addAction(
            name="check",
            display_name=_("Check"),
            tool_text=_("Check your code for mistakes."),
        )
        if sys.version_info[:2] >= (3, 6):
            self.addAction(
                name="tidy",
                display_name=_("Tidy"),
                tool_text=_("Tidy up the layout of your code."),
            )
        self.addAction(
            name="help",
            display_name=_("Help"),
            tool_text=_("Show help about Nu in a browser."),
        )
        self.addSeparator()
        self.addAction(name="quit", display_name=_("Quit"), tool_text=_("Quit Nu."))

    def set_responsive_mode(self, width, height):
        """
        Compact button bar for when window is very small.
        """
        font_size = min(DEFAULT_FONT_SIZE, width // 80)
        icon_size = min(64, width // 24)
        self.setIconSize(QSize(icon_size, icon_size))
        stylesheet = "QWidget{font-size: " + str(font_size) + "px;}"
        self.setStyleSheet(stylesheet)

    def addAction(self, name, display_name, tool_text):
        """
        Creates an action associated with an icon and name and adds it to the
        widget's slots.
        """
        action = QAction(load_icon(name), display_name, self, toolTip=tool_text)
        super().addAction(action)
        self.slots[name] = action

    def connect(self, name, handler, shortcut=None):
        """
        Connects a named slot to a handler function and optional hot-key
        shortcuts.
        """
        self.slots[name].pyqtConfigure(triggered=handler)
        if shortcut:
            self.slots[name].setShortcut(QKeySequence(shortcut))

addAction(name, display_name, tool_text)

Creates an action associated with an icon and name and adds it to the widget's slots.

Source code in mu/interface/main.py
def addAction(self, name, display_name, tool_text):
    """
    Creates an action associated with an icon and name and adds it to the
    widget's slots.
    """
    action = QAction(load_icon(name), display_name, self, toolTip=tool_text)
    super().addAction(action)
    self.slots[name] = action

connect(name, handler, shortcut=None)

Connects a named slot to a handler function and optional hot-key shortcuts.

Source code in mu/interface/main.py
def connect(self, name, handler, shortcut=None):
    """
    Connects a named slot to a handler function and optional hot-key
    shortcuts.
    """
    self.slots[name].pyqtConfigure(triggered=handler)
    if shortcut:
        self.slots[name].setShortcut(QKeySequence(shortcut))

reset()

Resets the button states.

Source code in mu/interface/main.py
def reset(self):
    """
    Resets the button states.
    """
    self.slots = {}
    self.clear()

set_responsive_mode(width, height)

Compact button bar for when window is very small.

Source code in mu/interface/main.py
def set_responsive_mode(self, width, height):
    """
    Compact button bar for when window is very small.
    """
    font_size = min(DEFAULT_FONT_SIZE, width // 80)
    icon_size = min(64, width // 24)
    self.setIconSize(QSize(icon_size, icon_size))
    stylesheet = "QWidget{font-size: " + str(font_size) + "px;}"
    self.setStyleSheet(stylesheet)

FileTabs

Bases: QTabWidget

Extend the base class so we can override the removeTab behaviour.

Source code in mu/interface/main.py
class FileTabs(QTabWidget):
    """
    Extend the base class so we can override the removeTab behaviour.
    """

    def __init__(self):
        super(FileTabs, self).__init__()
        # We are implementing closable tabs manually
        self.setTabsClosable(False)
        self.setMovable(True)
        self.currentChanged.connect(self.change_tab)
        # Shrink icons to avoid giant tabs
        self.setIconSize(self.iconSize() * 0.75)

    def removeTab(self, tab_id):
        """
        Ask the user before closing the file.
        """
        window = self.nativeParentWidget()
        modified = self.widget(tab_id).isModified()
        if modified:
            msg = "There is un-saved work, closing the tab will cause you to lose it."
            if window.show_confirmation(msg) == QMessageBox.StandardButton.Cancel:
                return
        super(FileTabs, self).removeTab(tab_id)

    def addTab(self, widget, title):
        """
        Add a new tab to the switcher
        """
        # Proxy up to the real addTab
        tab_id = super(FileTabs, self).addTab(widget, title)
        # A widget to put the indicator and close button in
        container = QWidget()
        box = QHBoxLayout(container)
        # We don't want any margins on the layout, that would expand the tab
        box.setContentsMargins(0, 0, 0, 0)
        # Ensure some space between image and button
        box.setSpacing(6)
        # Counterintuitively QImage doesn't show an image, QLabel does
        state_lbl = QLabel(container)
        box.addWidget(state_lbl)
        state_lbl.setPixmap(load_pixmap("document", size=self.iconSize()))

        # Watch for status change to update the dirty indicator
        # We watch here as it's far easier to keep track of state_lbl
        # It does mean we assume all tabs are EditorPane
        @widget.modificationChanged.connect
        def on_modified():
            # Wrapped in a try/except because sometimes state_lbl is destroyed
            # by Qt before widget is GC'd by Python. Since there's no way to
            # easily check the status of state_lbl, wrapping in a try/except
            # for a RuntimeError is the simplest possible solution.
            try:
                if widget.isModified():
                    state_lbl.setPixmap(
                        load_pixmap("document-dirty", size=self.iconSize())
                    )
                else:
                    # This icon is actually empty
                    state_lbl.setPixmap(load_pixmap("document", size=self.iconSize()))
            except RuntimeError:  # pragma: no cover
                pass

        # Setup our own close button since we are overriding the built in one
        close_btn = QPushButton(container)
        box.addWidget(close_btn)
        close_btn.setToolTip(_("Close file"))
        close_btn.setFlat(True)
        close_btn.setIconSize(self.iconSize())
        close_btn.setIcon(load_icon("close-tab"))
        close_btn.show()

        # Handle 'clicked' events
        @close_btn.clicked.connect
        def close():
            # The tab_id isn't constant and may have changed, lookup the
            # current id of the EditorPane
            tab_id = self.indexOf(widget)
            # Close the tab
            self.removeTab(tab_id)

        container.setLayout(box)
        # Add the box, clearly it isn't a button but QTabBar actually takes
        # any QWidget not just buttons
        self.tabBar().setTabButton(tab_id, QTabBar.ButtonPosition.RightSide, container)

        # Return the index of the new page just like the real addTab
        return tab_id

    def change_tab(self, tab_id):
        """
        Update the application title to reflect the name of the file in the
        currently selected tab.
        """
        current_tab = self.widget(tab_id)
        window = self.nativeParentWidget()
        if current_tab:
            window.update_title(current_tab.title)
        else:
            window.update_title(None)

addTab(widget, title)

Add a new tab to the switcher

Source code in mu/interface/main.py
def addTab(self, widget, title):
    """
    Add a new tab to the switcher
    """
    # Proxy up to the real addTab
    tab_id = super(FileTabs, self).addTab(widget, title)
    # A widget to put the indicator and close button in
    container = QWidget()
    box = QHBoxLayout(container)
    # We don't want any margins on the layout, that would expand the tab
    box.setContentsMargins(0, 0, 0, 0)
    # Ensure some space between image and button
    box.setSpacing(6)
    # Counterintuitively QImage doesn't show an image, QLabel does
    state_lbl = QLabel(container)
    box.addWidget(state_lbl)
    state_lbl.setPixmap(load_pixmap("document", size=self.iconSize()))

    # Watch for status change to update the dirty indicator
    # We watch here as it's far easier to keep track of state_lbl
    # It does mean we assume all tabs are EditorPane
    @widget.modificationChanged.connect
    def on_modified():
        # Wrapped in a try/except because sometimes state_lbl is destroyed
        # by Qt before widget is GC'd by Python. Since there's no way to
        # easily check the status of state_lbl, wrapping in a try/except
        # for a RuntimeError is the simplest possible solution.
        try:
            if widget.isModified():
                state_lbl.setPixmap(
                    load_pixmap("document-dirty", size=self.iconSize())
                )
            else:
                # This icon is actually empty
                state_lbl.setPixmap(load_pixmap("document", size=self.iconSize()))
        except RuntimeError:  # pragma: no cover
            pass

    # Setup our own close button since we are overriding the built in one
    close_btn = QPushButton(container)
    box.addWidget(close_btn)
    close_btn.setToolTip(_("Close file"))
    close_btn.setFlat(True)
    close_btn.setIconSize(self.iconSize())
    close_btn.setIcon(load_icon("close-tab"))
    close_btn.show()

    # Handle 'clicked' events
    @close_btn.clicked.connect
    def close():
        # The tab_id isn't constant and may have changed, lookup the
        # current id of the EditorPane
        tab_id = self.indexOf(widget)
        # Close the tab
        self.removeTab(tab_id)

    container.setLayout(box)
    # Add the box, clearly it isn't a button but QTabBar actually takes
    # any QWidget not just buttons
    self.tabBar().setTabButton(tab_id, QTabBar.ButtonPosition.RightSide, container)

    # Return the index of the new page just like the real addTab
    return tab_id

change_tab(tab_id)

Update the application title to reflect the name of the file in the currently selected tab.

Source code in mu/interface/main.py
def change_tab(self, tab_id):
    """
    Update the application title to reflect the name of the file in the
    currently selected tab.
    """
    current_tab = self.widget(tab_id)
    window = self.nativeParentWidget()
    if current_tab:
        window.update_title(current_tab.title)
    else:
        window.update_title(None)

removeTab(tab_id)

Ask the user before closing the file.

Source code in mu/interface/main.py
def removeTab(self, tab_id):
    """
    Ask the user before closing the file.
    """
    window = self.nativeParentWidget()
    modified = self.widget(tab_id).isModified()
    if modified:
        msg = "There is un-saved work, closing the tab will cause you to lose it."
        if window.show_confirmation(msg) == QMessageBox.StandardButton.Cancel:
            return
    super(FileTabs, self).removeTab(tab_id)

StatusBar

Bases: QStatusBar

Defines the look and behaviour of the status bar along the bottom of the UI.

Source code in mu/interface/main.py
class StatusBar(QStatusBar):
    """
    Defines the look and behaviour of the status bar along the bottom of the
    UI.
    """

    def __init__(self, parent=None, mode="python"):
        super().__init__(parent)
        self.mode = mode
        self.msg_duration = 5

        # Mode selector.
        self.mode_label = QLabel()
        self.mode_label.setToolTip(_("Nu's current mode of behaviour."))
        self.addPermanentWidget(self.mode_label)
        self.set_mode(mode)

        # Device selector.
        self.device_selector = DeviceSelector()
        self.device_selector.setHidden(True)
        self.addPermanentWidget(self.device_selector)

        # Logs viewer
        self.logs_label = QLabel()
        self.logs_label.setObjectName("AdministrationLabel")
        self.logs_label.setPixmap(load_pixmap("logs", size=QSize(24, 24)))
        self.logs_label.setToolTip(_("Nu Administration"))
        self.addPermanentWidget(self.logs_label)

    def connect_logs(self, handler, shortcut):
        """
        Connect the mouse press event and keyboard shortcut for the log widget
        to the referenced handler function.
        """
        self.logs_label.shortcut = QShortcut(QKeySequence(shortcut), self.parent())
        self.logs_label.shortcut.activated.connect(handler)
        self.logs_label.mousePressEvent = handler

    def connect_mode(self, handler, shortcut):
        """
        Connect the mouse press event and keyboard shortcut for the mode widget
        to the referenced handler function.
        """
        self.mode_label.shortcut = QShortcut(QKeySequence(shortcut), self.parent())
        self.mode_label.shortcut.activated.connect(handler)
        self.mode_label.mousePressEvent = handler

    def set_message(self, message, pause=5000):
        """
        Displays a message in the status bar for a certain period of time.
        """
        self.showMessage(message, pause)

    def set_mode(self, mode):
        """
        Updates the mode label to the new mode.
        """
        self.mode_label.setText(mode)

    def device_connected(self, device):
        """
        Show a tooltip whenever a new device connects
        """
        if device.board_name:
            msg = _("Detected new {} device: {}.").format(
                device.long_mode_name, device.board_name
            )
        else:
            msg = _("Detected new {} device.").format(device.long_mode_name)

        self.set_message(msg, self.msg_duration * 1000)

connect_logs(handler, shortcut)

Connect the mouse press event and keyboard shortcut for the log widget to the referenced handler function.

Source code in mu/interface/main.py
def connect_logs(self, handler, shortcut):
    """
    Connect the mouse press event and keyboard shortcut for the log widget
    to the referenced handler function.
    """
    self.logs_label.shortcut = QShortcut(QKeySequence(shortcut), self.parent())
    self.logs_label.shortcut.activated.connect(handler)
    self.logs_label.mousePressEvent = handler

connect_mode(handler, shortcut)

Connect the mouse press event and keyboard shortcut for the mode widget to the referenced handler function.

Source code in mu/interface/main.py
def connect_mode(self, handler, shortcut):
    """
    Connect the mouse press event and keyboard shortcut for the mode widget
    to the referenced handler function.
    """
    self.mode_label.shortcut = QShortcut(QKeySequence(shortcut), self.parent())
    self.mode_label.shortcut.activated.connect(handler)
    self.mode_label.mousePressEvent = handler

device_connected(device)

Show a tooltip whenever a new device connects

Source code in mu/interface/main.py
def device_connected(self, device):
    """
    Show a tooltip whenever a new device connects
    """
    if device.board_name:
        msg = _("Detected new {} device: {}.").format(
            device.long_mode_name, device.board_name
        )
    else:
        msg = _("Detected new {} device.").format(device.long_mode_name)

    self.set_message(msg, self.msg_duration * 1000)

set_message(message, pause=5000)

Displays a message in the status bar for a certain period of time.

Source code in mu/interface/main.py
def set_message(self, message, pause=5000):
    """
    Displays a message in the status bar for a certain period of time.
    """
    self.showMessage(message, pause)

set_mode(mode)

Updates the mode label to the new mode.

Source code in mu/interface/main.py
def set_mode(self, mode):
    """
    Updates the mode label to the new mode.
    """
    self.mode_label.setText(mode)

Window

Bases: QMainWindow

Defines the look and characteristics of the application's main window.

Source code in mu/interface/main.py
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
class Window(QMainWindow):
    """
    Defines the look and characteristics of the application's main window.
    """

    title = _("Nu {}").format(__version__)
    icon = "icon"
    timer = None
    usb_checker = None
    repl = None
    plotter = None
    zooms = ("xs", "s", "m", "l", "xl", "xxl", "xxxl")  # levels of zoom.
    zoom_position = 2  # current level of zoom (as position in zooms tuple).

    _zoom_in = pyqtSignal(str)
    _zoom_out = pyqtSignal(str)
    data_received = pyqtSignal(bytes)
    open_file = pyqtSignal(str)
    load_theme = pyqtSignal(str)
    previous_folder = None
    debug_widths = None

    def __init__(self, parent=None):
        super().__init__(parent)
        # Record pane area to allow reopening where user put it in a session
        self._debugger_area = 0
        self._inspector_area = 0
        self._plotter_area = 0
        self._repl_area = 0
        self._runner_area = 0
        self.progress = None

    def wheelEvent(self, event):
        """
        Trap a CTRL-scroll event so the user is able to zoom in and out.
        """
        modifiers = QApplication.keyboardModifiers()
        if modifiers == Qt.KeyboardModifier.ControlModifier:
            zoom = event.angleDelta().y() > 0
            if zoom:
                self.zoom_in()
            else:
                self.zoom_out()
            event.ignore()

    def set_zoom(self):
        """
        Sets the zoom to current zoom_position level.
        """
        self._zoom_in.emit(self.zooms[self.zoom_position])

    def zoom_in(self):
        """
        Handles zooming in.
        """
        self.zoom_position = min(self.zoom_position + 1, len(self.zooms) - 1)
        self._zoom_in.emit(self.zooms[self.zoom_position])

    def zoom_out(self):
        """
        Handles zooming out.
        """
        self.zoom_position = max(self.zoom_position - 1, 0)
        self._zoom_out.emit(self.zooms[self.zoom_position])

    def connect_zoom(self, widget):
        """
        Connects a referenced widget to the zoom related signals and sets
        the zoom of the widget to the current zoom level.
        """
        self._zoom_in.connect(widget.set_zoom)
        self._zoom_out.connect(widget.set_zoom)
        widget.set_zoom(self.zooms[self.zoom_position])

    @property
    def current_tab(self):
        """
        Returns the currently focussed tab.
        """
        return self.tabs.currentWidget()

    def set_read_only(self, is_readonly):
        """
        Set all tabs read-only.
        """
        self.read_only_tabs = is_readonly
        for tab in self.widgets:
            tab.setReadOnly(is_readonly)

    def get_load_path(self, folder, extensions="*", allow_previous=True):
        """
        Displays a dialog for selecting a file to load. Returns the selected
        path. Defaults to start in the referenced folder unless a previous
        folder has been used and the allow_previous flag is True (the default
        behaviour)
        """
        if allow_previous:
            open_in = folder if self.previous_folder is None else self.previous_folder
        else:
            open_in = folder
        path, _ = QFileDialog.getOpenFileName(
            self.widget, "Open file", open_in, extensions
        )
        logger.debug("Getting load path: {}".format(path))
        if allow_previous:
            self.previous_folder = os.path.dirname(path)
        return path

    def get_save_path(self, folder):
        """
        Displays a dialog for selecting a file to save. Returns the selected
        path. Defaults to start in the referenced folder.
        """
        path, _ = QFileDialog.getSaveFileName(
            self.widget,
            "Save file",
            folder if self.previous_folder is None else self.previous_folder,
            "Python (*.py);;Other (*.*)",
            "Python (*.py)",
        )
        self.previous_folder = os.path.dirname(path)
        # Ensure there's a .py extension if none is provided by the user.
        # See issue #1571.
        name, ext = os.path.splitext(os.path.basename(path))
        if name and (not name.startswith(".")) and (not ext):
            # The file is not a . (dot) file and there's no extension, so add
            # .py as default.
            path += ".py"
        logger.debug("Getting save path: {}".format(path))
        return path

    def add_tab(self, path, text, api, newline):
        """
        Adds a tab with the referenced path and text to the editor.
        """
        new_tab = EditorPane(path, text, newline)
        new_tab.connect_margin(self.breakpoint_toggle)
        new_tab_index = self.tabs.addTab(new_tab, new_tab.label)
        new_tab.set_api(api)

        @new_tab.modificationChanged.connect
        def on_modified():
            modified_tab_index = self.tabs.indexOf(new_tab)
            # Update tab label & window title
            # Tab dirty indicator is managed in FileTabs.addTab
            self.tabs.setTabText(modified_tab_index, new_tab.label)
            self.update_title(new_tab.title)

        @new_tab.open_file.connect
        def on_open_file(file):
            # Bubble the signal up
            self.open_file.emit(file)

        new_tab.context_menu.connect(self.on_context_menu)

        self.tabs.setCurrentIndex(new_tab_index)
        self.connect_zoom(new_tab)
        self.set_theme(self.theme)
        new_tab.setFocus()
        if self.read_only_tabs:
            new_tab.setReadOnly(self.read_only_tabs)
        return new_tab

    def focus_tab(self, tab):
        """
        Force focus on the referenced tab.
        """
        index = self.tabs.indexOf(tab)
        self.tabs.setCurrentIndex(index)
        tab.setFocus()

    @property
    def tab_count(self):
        """
        Returns the number of active tabs.
        """
        return self.tabs.count()

    @property
    def widgets(self):
        """
        Returns a list of references to the widgets representing tabs in the
        editor.
        """
        return [self.tabs.widget(i) for i in range(self.tab_count)]

    @property
    def modified(self):
        """
        Returns a boolean indication if there are any modified tabs in the
        editor.
        """
        for widget in self.widgets:
            if widget.isModified():
                return True
        return False

    def on_context_menu(self):
        """
        Called when a user right-clicks on an editor pane.

        If the REPL is active AND there is selected text in the current editor
        pane, modify the default context menu to include a paste to REPL
        option. Otherwise, just display the default context menu.
        """
        # Create a standard context menu.
        menu = self.current_tab.createStandardContextMenu()
        # Flag to indicate if there is a section of text highlighted.
        has_selection = any([x > -1 for x in self.current_tab.getSelection()])
        # Flag to indicate if the REPL pane is active.
        has_repl = hasattr(self, "repl") and self.repl
        # Flag to indicate if the Python process runner pane is active and in
        # interactive Python3 mode (pasting into other "standard" Python modes
        # doesn't make sense).
        has_runner = (
            hasattr(self, "process_runner")
            and self.process_runner
            and self.process_runner.is_interactive
        )
        if has_selection and (has_repl or has_runner):
            # Text selected with REPL/process context, so add the bespoke
            # "copy to repl" item to the standard context menu to handle this
            # situation.
            actions = menu.actions()
            copy_to_repl = QAction(_("Copy selected text to REPL"), menu)
            copy_to_repl.triggered.connect(self.copy_to_repl)
            menu.insertAction(actions[0], copy_to_repl)
            menu.insertSeparator(actions[0])
        # Display menu.
        menu.exec(QCursor.pos())

    def copy_to_repl(self):
        """
        Copies currently selected text in the editor, into the active REPL
        widget and sets focus to the REPL widget. The final line pasted into
        the REPL waits for RETURN to be pressed by the user (this appears to be
        the default behaviour for pasting into the REPL widget).
        """
        line_from, pos_from, line_to, pos_to = self.current_tab.getSelection()
        lines = self.current_tab.text().split("\n")
        if line_from == line_to:
            # Paste a fragment from an individual line.
            to_paste = lines[line_from][pos_from:pos_to]
        else:
            # Multi-line paste (paste all lines selected).
            selected = lines[line_from : line_to + 1]
            # Ensure the correct indentation.
            # If the first line starts with whitespace, work out the number of
            # spaces and deduct said number of whitespace from start of all
            # other lines. That's perhaps the best we can do.
            if selected and selected[0].startswith(" "):
                indent = 0
                for char in selected[0]:
                    if char.isspace():
                        indent += 1
                    else:
                        break
                selected = [line[indent:] for line in selected]
            to_paste = "\n".join(selected)
        logger.info("Pasting to REPL")
        logger.info("\n" + to_paste)
        clipboard = QApplication.clipboard()
        clipboard.setText(to_paste)
        if hasattr(self, "repl_pane") and self.repl_pane:
            self.repl_pane.paste()
            self.repl_pane.setFocus()
        elif hasattr(self, "process_runner") and self.process_runner:
            self.process_runner.paste()
            self.process_runner.setFocus()

    def on_stdout_write(self, data):
        """
        Called when either a running script or the REPL write to STDOUT.
        """
        self.data_received.emit(data)

    def add_micropython_repl(self, name, connection):
        """
        Adds a MicroPython based REPL pane to the application.
        """
        repl_pane = MicroPythonREPLPane(connection)
        connection.data_received.connect(repl_pane.process_tty_data)
        self.add_repl(repl_pane, name)

    def add_micropython_plotter(self, name, connection, data_flood_handler):
        """
        Adds a plotter that reads data from a serial connection.
        """
        plotter_pane = PlotterPane()
        connection.data_received.connect(plotter_pane.process_tty_data)
        plotter_pane.data_flood.connect(data_flood_handler)
        self.add_plotter(plotter_pane, name)

    def add_python3_plotter(self, mode):
        """
        Add a plotter that reads from either the REPL or a running script.
        Since this function will only be called when either the REPL or a
        running script are running (but not at the same time), it'll just grab
        data emitted by the REPL or script via data_received.
        """
        plotter_pane = PlotterPane()
        self.data_received.connect(plotter_pane.process_tty_data)
        plotter_pane.data_flood.connect(mode.on_data_flood)
        self.add_plotter(plotter_pane, _("Python3 data tuple"))

    def add_jupyter_repl(self, kernel_manager, kernel_client):
        """
        Adds a Jupyter based REPL pane to the application.
        """
        kernel_manager.kernel.gui = "qt4"
        kernel_client.start_channels()
        ipython_widget = JupyterREPLPane()
        ipython_widget.kernel_manager = kernel_manager
        ipython_widget.kernel_client = kernel_client
        ipython_widget.on_append_text.connect(self.on_stdout_write)
        self.add_repl(ipython_widget, _("Python3 (Jupyter)"))

    def add_repl(self, repl_pane, name):
        """
        Adds the referenced REPL pane to the application.
        """
        self.repl_pane = repl_pane
        self.repl = QDockWidget(_("{} REPL").format(name))
        self.repl.setWidget(repl_pane)
        self.repl.setFeatures(QDockWidget.DockWidgetFeature.DockWidgetMovable)
        self.repl.setAllowedAreas(
            Qt.DockWidgetArea.BottomDockWidgetArea
            | Qt.DockWidgetArea.LeftDockWidgetArea
            | Qt.DockWidgetArea.RightDockWidgetArea
        )
        area = self._repl_area or Qt.DockWidgetArea.BottomDockWidgetArea
        self.addDockWidget(area, self.repl)
        self.connect_zoom(self.repl_pane)
        self.repl_pane.set_theme(self.theme)
        self.repl_pane.setFocus()

    def add_plotter(self, plotter_pane, name):
        """
        Adds the referenced plotter pane to the application.
        """
        self.plotter_pane = plotter_pane
        self.plotter = QDockWidget(_("{} Plotter").format(name))
        self.plotter.setWidget(plotter_pane)
        self.plotter.setFeatures(QDockWidget.DockWidgetFeature.DockWidgetMovable)
        self.plotter.setAllowedAreas(
            Qt.DockWidgetArea.BottomDockWidgetArea
            | Qt.DockWidgetArea.LeftDockWidgetArea
            | Qt.DockWidgetArea.RightDockWidgetArea
        )
        area = self._plotter_area or Qt.DockWidgetArea.BottomDockWidgetArea
        self.addDockWidget(area, self.plotter)
        self.plotter_pane.set_theme(self.theme)
        self.plotter_pane.setFocus()

    def add_python3_runner(
        self,
        interpreter,
        script_name,
        working_directory,
        interactive=False,
        debugger=False,
        command_args=None,
        envars=None,
        python_args=None,
    ):
        """
        Display console output for the interpreter with the referenced
        pythonpath running the referenced script.

        The script will be run within the workspace_path directory.

        If interactive is True (default is False) the Python process will
        run in interactive mode (dropping the user into the REPL when the
        script completes).

        If debugger is True (default is False) the script will be run within
        a debug runner session. The debugger overrides the interactive flag
        (you cannot run the debugger in interactive mode).

        If there is a list of command_args (the default is None) then these
        will be passed as further arguments into the command run in the
        new process.

        If envars is given, these will become part of the environment context
        of the new child process.

        If python_args is given, these will be passed as arguments to the
        Python runtime used to launch the child process.
        """
        self.process_runner = PythonProcessPane(self)
        self.runner = QDockWidget(
            _("Running: {}").format(os.path.basename(script_name))
        )
        self.runner.setWidget(self.process_runner)
        self.runner.setFeatures(QDockWidget.DockWidgetFeature.DockWidgetMovable)
        self.runner.setAllowedAreas(
            Qt.DockWidgetArea.BottomDockWidgetArea
            | Qt.DockWidgetArea.LeftDockWidgetArea
            | Qt.DockWidgetArea.RightDockWidgetArea
        )
        self.process_runner.debugger = debugger
        if debugger:
            area = self._debugger_area or Qt.DockWidgetArea.BottomDockWidgetArea
        else:
            area = self._runner_area or Qt.DockWidgetArea.BottomDockWidgetArea
        self.addDockWidget(area, self.runner)
        logger.info(
            "About to start_process: %r, %r, %r, %r, %r, %r, %r, %r",
            interpreter,
            script_name,
            working_directory,
            interactive,
            debugger,
            command_args,
            envars,
            python_args,
        )

        self.process_runner.start_process(
            interpreter,
            script_name,
            working_directory,
            interactive,
            debugger,
            command_args,
            envars,
            python_args,
        )
        self.process_runner.setFocus()
        self.process_runner.on_append_text.connect(self.on_stdout_write)
        self.connect_zoom(self.process_runner)
        return self.process_runner

    def add_debug_inspector(self):
        """
        Display a debug inspector to view the call stack.
        """
        self.debug_inspector = DebugInspector()
        self.debug_model = QStandardItemModel()
        self.debug_inspector.setModel(self.debug_model)
        self.inspector = QDockWidget(_("Debug Inspector"))
        self.inspector.setWidget(self.debug_inspector)
        self.inspector.setFeatures(QDockWidget.DockWidgetFeature.DockWidgetMovable)
        self.inspector.setAllowedAreas(
            Qt.DockWidgetArea.BottomDockWidgetArea
            | Qt.DockWidgetArea.LeftDockWidgetArea
            | Qt.DockWidgetArea.RightDockWidgetArea
        )
        area = self._inspector_area or Qt.DockWidgetArea.RightDockWidgetArea
        self.addDockWidget(area, self.inspector)
        self.connect_zoom(self.debug_inspector)
        # Setup the inspector headers and restore column widths
        self.debug_model.setHorizontalHeaderLabels([_("Name"), _("Value")])
        if self.debug_widths:
            for col, width in enumerate(self.debug_widths):
                self.debug_inspector.setColumnWidth(col, width)

    def update_debug_inspector(self, locals_dict):
        """
        Given the contents of a dict representation of the locals in the
        current stack frame, update the debug inspector with the new values.
        """
        excluded_names = ["__builtins__", "__debug_code__", "__debug_script__"]
        names = sorted([x for x in locals_dict if x not in excluded_names])

        # Remove rows so we keep the same column layouts if manually set
        while self.debug_model.rowCount() > 0:
            self.debug_model.removeRow(0)
        for name in names:
            item_to_expand = None
            try:
                # DANGER!
                val = eval(locals_dict[name])
            except Exception:
                val = None
            if isinstance(val, list):
                # Show a list consisting of rows of position/value
                list_item = DebugInspectorItem(name)
                item_to_expand = list_item
                for i, i_val in enumerate(val):
                    list_item.appendRow(
                        [
                            DebugInspectorItem(str(i)),
                            DebugInspectorItem(repr(i_val)),
                        ]
                    )
                self.debug_model.appendRow(
                    [
                        list_item,
                        DebugInspectorItem(_("(A list of {} items.)").format(len(val))),
                    ]
                )
            elif isinstance(val, dict):
                # Show a dict consisting of rows of key/value pairs.
                dict_item = DebugInspectorItem(name)
                item_to_expand = dict_item
                for k, k_val in val.items():
                    dict_item.appendRow(
                        [
                            DebugInspectorItem(repr(k)),
                            DebugInspectorItem(repr(k_val)),
                        ]
                    )
                self.debug_model.appendRow(
                    [
                        dict_item,
                        DebugInspectorItem(_("(A dict of {} items.)").format(len(val))),
                    ]
                )
            else:
                self.debug_model.appendRow(
                    [
                        DebugInspectorItem(name),
                        DebugInspectorItem(locals_dict[name]),
                    ]
                )
            # Expand dicts/list with names matching old expanded entries
            if (
                hasattr(self, "debug_inspector")
                and name in self.debug_inspector.expanded_dicts
                and item_to_expand is not None
            ):
                self.debug_inspector.expand(
                    self.debug_model.indexFromItem(item_to_expand)
                )

    def remove_repl(self):
        """
        Removes the REPL pane from the application.
        """
        if self.repl:
            self._repl_area = self.dockWidgetArea(self.repl)
            self.repl_pane = None
            self.repl.setParent(None)
            self.repl.deleteLater()
            self.repl = None

    def remove_plotter(self):
        """
        Removes the plotter pane from the application.
        """
        if self.plotter:
            self._plotter_area = self.dockWidgetArea(self.plotter)
            self.plotter_pane = None
            self.plotter.setParent(None)
            self.plotter.deleteLater()
            self.plotter = None

    def remove_python_runner(self):
        """
        Removes the runner pane from the application.
        """
        if hasattr(self, "runner") and self.runner:
            if self.process_runner.debugger:
                self._debugger_area = self.dockWidgetArea(self.runner)
            else:
                self._runner_area = self.dockWidgetArea(self.runner)
            self.process_runner = None
            self.runner.setParent(None)
            self.runner.deleteLater()
            self.runner = None

    def remove_debug_inspector(self):
        """
        Removes the debug inspector pane from the application.
        """
        if hasattr(self, "inspector") and self.inspector:
            width = self.debug_inspector.columnWidth
            self.debug_widths = width(0), width(1)
            self._inspector_area = self.dockWidgetArea(self.inspector)
            self.debug_inspector = None
            self.debug_model = None
            self.inspector.setParent(None)
            self.inspector.deleteLater()
            self.inspector = None

    def set_theme(self, theme):
        """
        Sets the theme for the REPL and editor tabs.
        """
        self.theme = theme
        self.load_theme.emit(theme)
        if theme == "contrast":
            new_theme = ContrastTheme
            new_icon = "theme_day"
        elif theme == "night":
            new_theme = NightTheme
            new_icon = "theme_contrast"
        else:
            new_theme = DayTheme
            new_icon = "theme"
        for widget in self.widgets:
            widget.set_theme(new_theme)
        self.button_bar.slots["theme"].setIcon(load_icon(new_icon))
        if hasattr(self, "repl") and self.repl:
            self.repl_pane.set_theme(theme)
        if hasattr(self, "plotter") and self.plotter:
            self.plotter_pane.set_theme(theme)
        if hasattr(self, "runner") and self.runner:
            self.process_runner.set_theme(theme)

    def set_checker_icon(self, icon):
        """
        Set the status icon to use on the check button
        """
        self.button_bar.slots["check"].setIcon(load_icon(icon))
        timer = QTimer()

        @timer.timeout.connect
        def reset():
            self.button_bar.slots["check"].setIcon(load_icon("check"))
            timer.stop()

        timer.start(500)

    def show_admin(self, log, settings, packages, mode, device_list):
        """
        Display the administrative dialog with referenced content of the log
        and settings. Return a dictionary of the settings that may have been
        changed by the admin dialog.
        """
        admin_box = AdminDialog(self)
        admin_box.setup(log, settings, packages, mode, device_list)
        result = admin_box.exec()
        if result:
            return admin_box.settings()
        else:
            return {}

    def sync_packages(self, to_remove, to_add):
        """
        Display a modal dialog that indicates the status of the add/remove
        package management operation.
        """
        package_box = PackageDialog(self)
        package_box.setup(to_remove, to_add)
        package_box.exec()

    def show_message(self, message, information=None, icon=None):
        """
        Displays a modal message to the user.

        If information is passed in this will be set as the additional
        informative text in the modal dialog.

        Since this mechanism will be used mainly for warning users that
        something is awry the default icon is set to "Warning". It's possible
        to override the icon to one of the following settings: NoIcon,
        Question, Information, Warning or Critical.
        """
        message_box = QMessageBox(self)
        message_box.setText(message)
        message_box.setWindowTitle("Nu")
        if information:
            message_box.setInformativeText(information)
        if icon and hasattr(QMessageBox.Icon, icon):
            message_box.setIcon(getattr(QMessageBox.Icon, icon))
        else:
            message_box.setIcon(QMessageBox.Icon.Warning)
        logger.debug(message)
        logger.debug(information)
        message_box.exec()

    def show_confirmation(self, message, information=None, icon=None):
        """
        Displays a modal message to the user to which they need to confirm or
        cancel.

        If information is passed in this will be set as the additional
        informative text in the modal dialog.

        Since this mechanism will be used mainly for warning users that
        something is awry the default icon is set to "Warning". It's possible
        to override the icon to one of the following settings: NoIcon,
        Question, Information, Warning or Critical.
        """
        message_box = QMessageBox(self)
        message_box.setText(message)
        message_box.setWindowTitle(_("Nu"))
        if information:
            message_box.setInformativeText(information)
        if icon and hasattr(QMessageBox.Icon, icon):
            message_box.setIcon(getattr(QMessageBox.Icon, icon))
        else:
            message_box.setIcon(QMessageBox.Icon.Warning)
        message_box.setStandardButtons(
            QMessageBox.StandardButton.Cancel | QMessageBox.StandardButton.Ok
        )
        message_box.setDefaultButton(QMessageBox.StandardButton.Cancel)
        logger.debug(message)
        logger.debug(information)
        return message_box.exec()

    def update_title(self, filename=None):
        """
        Updates the title bar of the application. If a filename (representing
        the name of the file currently the focus of the editor) is supplied,
        append it to the end of the title.
        """
        title = self.title
        if filename:
            title += " - " + filename
        self.setWindowTitle(title)

    def screen_size(self):
        """
        Returns an (width, height) tuple with the screen geometry.
        """
        screen = QApplication.primaryScreen().geometry()
        return screen.width(), screen.height()

    def size_window(self, x=None, y=None, w=None, h=None):
        """
        Makes the editor 80% of the width*height of the screen and centres it
        when none of x, y, w and h is passed in; otherwise uses the passed in
        values to position and size the editor window.

        If the X or Y value will be off the screen, these are reset to None
        (thus stopping the window being drawn in a hard-to-reach place). See
        issue #1613 for context.
        """
        screen_width, screen_height = self.screen_size()
        w = int(screen_width * 0.8) if w is None else w
        h = int(screen_height * 0.8) if h is None else h
        self.resize(w, h)
        size = self.geometry()
        # Ensure the window isn't added off the screen.
        if x and (x <= 0 or x > screen_width):
            x = None
        if y and (y <= 0 or y > screen_height):
            y = None
        x = (screen_width - size.width()) // 2 if x is None else x
        y = (screen_height - size.height()) // 2 if y is None else y
        self.move(x, y)

    def reset_annotations(self):
        """
        Resets the state of annotations on the current tab.
        """
        self.current_tab.reset_annotations()

    def annotate_code(self, feedback, annotation_type):
        """
        Given a list of annotations about the code in the current tab, add
        the annotations to the editor window so the user can make appropriate
        changes.
        """
        self.current_tab.annotate_code(feedback, annotation_type)

    def show_annotations(self):
        """
        Show the annotations added to the current tab.
        """
        self.current_tab.show_annotations()

    def setup(self, breakpoint_toggle, theme):
        """
        Sets up the window.

        Defines the various attributes of the window and defines how the user
        interface is laid out.
        """
        self.theme = theme
        self.breakpoint_toggle = breakpoint_toggle
        # Give the window a default icon, title and minimum size.
        self.setWindowIcon(load_icon(self.icon))
        self.update_title()
        self.read_only_tabs = False
        screen_width, screen_height = self.screen_size()
        self.setMinimumSize(
            min(screen_width // 2, MIN_WINDOW_WIDTH),
            min(screen_height // 2, MIN_WINDOW_HEIGHT),
        )
        self.setTabPosition(
            Qt.DockWidgetArea.AllDockWidgetAreas, QTabWidget.TabPosition.North
        )
        self.widget = QWidget()
        widget_layout = QVBoxLayout()
        self.widget.setLayout(widget_layout)
        self.button_bar = ButtonBar(self.widget)
        self.tabs = FileTabs()
        self.setCentralWidget(self.tabs)
        self.status_bar = StatusBar(parent=self)
        self.setStatusBar(self.status_bar)
        self.addToolBar(self.button_bar)
        self.show()

    def resizeEvent(self, resizeEvent):
        """
        Respond to window getting too small for the button bar to fit well.
        """
        size = resizeEvent.size()
        self.button_bar.set_responsive_mode(size.width(), size.height())

    def select_mode(self, modes, current_mode):
        """
        Display the mode selector dialog and return the result.
        """
        mode_select = ModeSelector(self)
        mode_select.setup(modes, current_mode)
        mode_select.exec()
        try:
            return mode_select.get_mode()
        except Exception:
            return None

    def change_mode(self, mode):
        """
        Given a an object representing a mode, recreates the button bar with
        the expected functionality.
        """
        self.button_bar.change_mode(mode)
        # Update the autocomplete / tooltip APIs for each tab to the new mode.
        api = mode.api()
        for widget in self.widgets:
            widget.set_api(api)

    def set_usb_checker(self, duration, callback):
        """
        Sets up a timer that polls for USB changes via the "callback" every
        "duration" seconds.
        """
        self.usb_checker = QTimer()
        self.usb_checker.timeout.connect(callback)
        self.usb_checker.start(duration * 1000)

    def set_timer(self, duration, callback):
        """
        Set a repeating timer to call "callback" every "duration" seconds.
        """
        self.timer = QTimer()
        self.timer.timeout.connect(callback)
        self.timer.start(duration * 1000)  # Measured in milliseconds.

    def stop_timer(self):
        """
        Stop the repeating timer.
        """
        if self.timer:
            self.timer.stop()
            self.timer = None

    def connect_tab_rename(self, handler, shortcut):
        """
        Connect the double-click event on a tab and the keyboard shortcut to
        the referenced handler (causing the Save As dialog).
        """
        self.tabs.shortcut = QShortcut(QKeySequence(shortcut), self)
        self.tabs.shortcut.activated.connect(handler)
        self.tabs.tabBarDoubleClicked.connect(handler)

    def open_directory_from_os(self, path):
        """
        Given the path to a directory, open the OS's built in filesystem
        explorer for that path. Works with Windows, OSX and Linux.
        """
        if sys.platform == "win32":
            # Windows
            os.startfile(path)
        elif sys.platform == "darwin":
            # OSX
            os.system('open "{}"'.format(path))
        else:
            # Assume freedesktop.org on unix-y.
            os.system('xdg-open "{}"'.format(path))

    def connect_find_replace(self, handler, shortcut):
        """
        Create a keyboard shortcut and associate it with a handler for doing
        a find and replace.
        """
        self.find_replace_shortcut = QShortcut(QKeySequence(shortcut), self)
        self.find_replace_shortcut.activated.connect(handler)

    def connect_find_again(self, handlers, shortcut):
        """
        Create keyboard shortcuts and associate them with handlers for doing
        a find again in forward or backward direction. Any given shortcut
        will be used for forward find again, while Shift+shortcut will find
        again backwards.
        """
        forward, backward = handlers
        self.find_again_shortcut = QShortcut(QKeySequence(shortcut), self)
        self.find_again_shortcut.activated.connect(forward)
        backward_shortcut = QKeySequence("Shift+" + shortcut)
        self.find_again_backward_shortcut = QShortcut(backward_shortcut, self)
        self.find_again_backward_shortcut.activated.connect(backward)

    def show_find_replace(self, find, replace, global_replace):
        """
        Display the find/replace dialog. If the dialog's OK button was clicked
        return a tuple containing the find term, replace term and global
        replace flag.
        """
        finder = FindReplaceDialog(self)
        finder.setup(find, replace, global_replace)
        if finder.exec():
            return (finder.find(), finder.replace(), finder.replace_flag())

    def replace_text(self, target_text, replace, global_replace):
        """
        Given target_text, replace the first instance after the cursor with
        "replace". If global_replace is true, replace all instances of
        "target". Returns the number of times replacement has occurred.
        """
        if not self.current_tab:
            return 0
        if global_replace:
            counter = 0
            found = self.current_tab.findFirst(
                target_text, False, True, False, False, line=0, index=0
            )
            if found:
                counter += 1
                self.current_tab.replace(replace)
                while self.current_tab.findNext():
                    self.current_tab.replace(replace)
                    counter += 1
            return counter
        else:
            found = self.current_tab.findFirst(target_text, False, True, False, True)
            if found:
                self.current_tab.replace(replace)
                return 1
            else:
                return 0

    def highlight_text(self, target_text, forward=True):
        """
        Highlight the first match from the current position of the cursor in
        the current tab for the target_text. Returns True if there's a match.
        """
        if self.current_tab:
            line = -1
            index = -1
            if not forward:
                # Workaround for `findFirst(forward=False)` not advancing
                # backwards: pass explicit line and index values.
                line, index, _el, _ei = self.current_tab.getSelection()
            return self.current_tab.findFirst(
                target_text,  # Text to find,
                False,  # Treat as regular expression
                True,  # Case sensitive search
                False,  # Whole word matches only
                True,  # Wrap search
                forward=forward,  # Forward search
                line=line,  # -1 starts at current position
                index=index,  # -1 starts at current position
            )
        else:
            return False

    def connect_toggle_comments(self, handler, shortcut):
        """
        Create a keyboard shortcut and associate it with a handler for toggling
        comments on highlighted lines.
        """
        self.toggle_comments_shortcut = QShortcut(QKeySequence(shortcut), self)
        self.toggle_comments_shortcut.activated.connect(handler)

    def toggle_comments(self):
        """
        Toggle comments on/off for all selected line in the currently active
        tab.
        """
        if self.current_tab:
            self.current_tab.toggle_comments()

    def show_device_selector(self):
        """
        Reveals the device selector in the status bar
        """
        self.status_bar.device_selector.setHidden(False)

    def hide_device_selector(self):
        """
        Hides the device selector in the status bar
        """
        self.status_bar.device_selector.setHidden(True)

current_tab property

Returns the currently focussed tab.

modified property

Returns a boolean indication if there are any modified tabs in the editor.

tab_count property

Returns the number of active tabs.

widgets property

Returns a list of references to the widgets representing tabs in the editor.

add_debug_inspector()

Display a debug inspector to view the call stack.

Source code in mu/interface/main.py
def add_debug_inspector(self):
    """
    Display a debug inspector to view the call stack.
    """
    self.debug_inspector = DebugInspector()
    self.debug_model = QStandardItemModel()
    self.debug_inspector.setModel(self.debug_model)
    self.inspector = QDockWidget(_("Debug Inspector"))
    self.inspector.setWidget(self.debug_inspector)
    self.inspector.setFeatures(QDockWidget.DockWidgetFeature.DockWidgetMovable)
    self.inspector.setAllowedAreas(
        Qt.DockWidgetArea.BottomDockWidgetArea
        | Qt.DockWidgetArea.LeftDockWidgetArea
        | Qt.DockWidgetArea.RightDockWidgetArea
    )
    area = self._inspector_area or Qt.DockWidgetArea.RightDockWidgetArea
    self.addDockWidget(area, self.inspector)
    self.connect_zoom(self.debug_inspector)
    # Setup the inspector headers and restore column widths
    self.debug_model.setHorizontalHeaderLabels([_("Name"), _("Value")])
    if self.debug_widths:
        for col, width in enumerate(self.debug_widths):
            self.debug_inspector.setColumnWidth(col, width)

add_jupyter_repl(kernel_manager, kernel_client)

Adds a Jupyter based REPL pane to the application.

Source code in mu/interface/main.py
def add_jupyter_repl(self, kernel_manager, kernel_client):
    """
    Adds a Jupyter based REPL pane to the application.
    """
    kernel_manager.kernel.gui = "qt4"
    kernel_client.start_channels()
    ipython_widget = JupyterREPLPane()
    ipython_widget.kernel_manager = kernel_manager
    ipython_widget.kernel_client = kernel_client
    ipython_widget.on_append_text.connect(self.on_stdout_write)
    self.add_repl(ipython_widget, _("Python3 (Jupyter)"))

add_micropython_plotter(name, connection, data_flood_handler)

Adds a plotter that reads data from a serial connection.

Source code in mu/interface/main.py
def add_micropython_plotter(self, name, connection, data_flood_handler):
    """
    Adds a plotter that reads data from a serial connection.
    """
    plotter_pane = PlotterPane()
    connection.data_received.connect(plotter_pane.process_tty_data)
    plotter_pane.data_flood.connect(data_flood_handler)
    self.add_plotter(plotter_pane, name)

add_micropython_repl(name, connection)

Adds a MicroPython based REPL pane to the application.

Source code in mu/interface/main.py
def add_micropython_repl(self, name, connection):
    """
    Adds a MicroPython based REPL pane to the application.
    """
    repl_pane = MicroPythonREPLPane(connection)
    connection.data_received.connect(repl_pane.process_tty_data)
    self.add_repl(repl_pane, name)

add_plotter(plotter_pane, name)

Adds the referenced plotter pane to the application.

Source code in mu/interface/main.py
def add_plotter(self, plotter_pane, name):
    """
    Adds the referenced plotter pane to the application.
    """
    self.plotter_pane = plotter_pane
    self.plotter = QDockWidget(_("{} Plotter").format(name))
    self.plotter.setWidget(plotter_pane)
    self.plotter.setFeatures(QDockWidget.DockWidgetFeature.DockWidgetMovable)
    self.plotter.setAllowedAreas(
        Qt.DockWidgetArea.BottomDockWidgetArea
        | Qt.DockWidgetArea.LeftDockWidgetArea
        | Qt.DockWidgetArea.RightDockWidgetArea
    )
    area = self._plotter_area or Qt.DockWidgetArea.BottomDockWidgetArea
    self.addDockWidget(area, self.plotter)
    self.plotter_pane.set_theme(self.theme)
    self.plotter_pane.setFocus()

add_python3_plotter(mode)

Add a plotter that reads from either the REPL or a running script. Since this function will only be called when either the REPL or a running script are running (but not at the same time), it'll just grab data emitted by the REPL or script via data_received.

Source code in mu/interface/main.py
def add_python3_plotter(self, mode):
    """
    Add a plotter that reads from either the REPL or a running script.
    Since this function will only be called when either the REPL or a
    running script are running (but not at the same time), it'll just grab
    data emitted by the REPL or script via data_received.
    """
    plotter_pane = PlotterPane()
    self.data_received.connect(plotter_pane.process_tty_data)
    plotter_pane.data_flood.connect(mode.on_data_flood)
    self.add_plotter(plotter_pane, _("Python3 data tuple"))

add_python3_runner(interpreter, script_name, working_directory, interactive=False, debugger=False, command_args=None, envars=None, python_args=None)

Display console output for the interpreter with the referenced pythonpath running the referenced script.

The script will be run within the workspace_path directory.

If interactive is True (default is False) the Python process will run in interactive mode (dropping the user into the REPL when the script completes).

If debugger is True (default is False) the script will be run within a debug runner session. The debugger overrides the interactive flag (you cannot run the debugger in interactive mode).

If there is a list of command_args (the default is None) then these will be passed as further arguments into the command run in the new process.

If envars is given, these will become part of the environment context of the new child process.

If python_args is given, these will be passed as arguments to the Python runtime used to launch the child process.

Source code in mu/interface/main.py
def add_python3_runner(
    self,
    interpreter,
    script_name,
    working_directory,
    interactive=False,
    debugger=False,
    command_args=None,
    envars=None,
    python_args=None,
):
    """
    Display console output for the interpreter with the referenced
    pythonpath running the referenced script.

    The script will be run within the workspace_path directory.

    If interactive is True (default is False) the Python process will
    run in interactive mode (dropping the user into the REPL when the
    script completes).

    If debugger is True (default is False) the script will be run within
    a debug runner session. The debugger overrides the interactive flag
    (you cannot run the debugger in interactive mode).

    If there is a list of command_args (the default is None) then these
    will be passed as further arguments into the command run in the
    new process.

    If envars is given, these will become part of the environment context
    of the new child process.

    If python_args is given, these will be passed as arguments to the
    Python runtime used to launch the child process.
    """
    self.process_runner = PythonProcessPane(self)
    self.runner = QDockWidget(
        _("Running: {}").format(os.path.basename(script_name))
    )
    self.runner.setWidget(self.process_runner)
    self.runner.setFeatures(QDockWidget.DockWidgetFeature.DockWidgetMovable)
    self.runner.setAllowedAreas(
        Qt.DockWidgetArea.BottomDockWidgetArea
        | Qt.DockWidgetArea.LeftDockWidgetArea
        | Qt.DockWidgetArea.RightDockWidgetArea
    )
    self.process_runner.debugger = debugger
    if debugger:
        area = self._debugger_area or Qt.DockWidgetArea.BottomDockWidgetArea
    else:
        area = self._runner_area or Qt.DockWidgetArea.BottomDockWidgetArea
    self.addDockWidget(area, self.runner)
    logger.info(
        "About to start_process: %r, %r, %r, %r, %r, %r, %r, %r",
        interpreter,
        script_name,
        working_directory,
        interactive,
        debugger,
        command_args,
        envars,
        python_args,
    )

    self.process_runner.start_process(
        interpreter,
        script_name,
        working_directory,
        interactive,
        debugger,
        command_args,
        envars,
        python_args,
    )
    self.process_runner.setFocus()
    self.process_runner.on_append_text.connect(self.on_stdout_write)
    self.connect_zoom(self.process_runner)
    return self.process_runner

add_repl(repl_pane, name)

Adds the referenced REPL pane to the application.

Source code in mu/interface/main.py
def add_repl(self, repl_pane, name):
    """
    Adds the referenced REPL pane to the application.
    """
    self.repl_pane = repl_pane
    self.repl = QDockWidget(_("{} REPL").format(name))
    self.repl.setWidget(repl_pane)
    self.repl.setFeatures(QDockWidget.DockWidgetFeature.DockWidgetMovable)
    self.repl.setAllowedAreas(
        Qt.DockWidgetArea.BottomDockWidgetArea
        | Qt.DockWidgetArea.LeftDockWidgetArea
        | Qt.DockWidgetArea.RightDockWidgetArea
    )
    area = self._repl_area or Qt.DockWidgetArea.BottomDockWidgetArea
    self.addDockWidget(area, self.repl)
    self.connect_zoom(self.repl_pane)
    self.repl_pane.set_theme(self.theme)
    self.repl_pane.setFocus()

add_tab(path, text, api, newline)

Adds a tab with the referenced path and text to the editor.

Source code in mu/interface/main.py
def add_tab(self, path, text, api, newline):
    """
    Adds a tab with the referenced path and text to the editor.
    """
    new_tab = EditorPane(path, text, newline)
    new_tab.connect_margin(self.breakpoint_toggle)
    new_tab_index = self.tabs.addTab(new_tab, new_tab.label)
    new_tab.set_api(api)

    @new_tab.modificationChanged.connect
    def on_modified():
        modified_tab_index = self.tabs.indexOf(new_tab)
        # Update tab label & window title
        # Tab dirty indicator is managed in FileTabs.addTab
        self.tabs.setTabText(modified_tab_index, new_tab.label)
        self.update_title(new_tab.title)

    @new_tab.open_file.connect
    def on_open_file(file):
        # Bubble the signal up
        self.open_file.emit(file)

    new_tab.context_menu.connect(self.on_context_menu)

    self.tabs.setCurrentIndex(new_tab_index)
    self.connect_zoom(new_tab)
    self.set_theme(self.theme)
    new_tab.setFocus()
    if self.read_only_tabs:
        new_tab.setReadOnly(self.read_only_tabs)
    return new_tab

annotate_code(feedback, annotation_type)

Given a list of annotations about the code in the current tab, add the annotations to the editor window so the user can make appropriate changes.

Source code in mu/interface/main.py
def annotate_code(self, feedback, annotation_type):
    """
    Given a list of annotations about the code in the current tab, add
    the annotations to the editor window so the user can make appropriate
    changes.
    """
    self.current_tab.annotate_code(feedback, annotation_type)

change_mode(mode)

Given a an object representing a mode, recreates the button bar with the expected functionality.

Source code in mu/interface/main.py
def change_mode(self, mode):
    """
    Given a an object representing a mode, recreates the button bar with
    the expected functionality.
    """
    self.button_bar.change_mode(mode)
    # Update the autocomplete / tooltip APIs for each tab to the new mode.
    api = mode.api()
    for widget in self.widgets:
        widget.set_api(api)

connect_find_again(handlers, shortcut)

Create keyboard shortcuts and associate them with handlers for doing a find again in forward or backward direction. Any given shortcut will be used for forward find again, while Shift+shortcut will find again backwards.

Source code in mu/interface/main.py
def connect_find_again(self, handlers, shortcut):
    """
    Create keyboard shortcuts and associate them with handlers for doing
    a find again in forward or backward direction. Any given shortcut
    will be used for forward find again, while Shift+shortcut will find
    again backwards.
    """
    forward, backward = handlers
    self.find_again_shortcut = QShortcut(QKeySequence(shortcut), self)
    self.find_again_shortcut.activated.connect(forward)
    backward_shortcut = QKeySequence("Shift+" + shortcut)
    self.find_again_backward_shortcut = QShortcut(backward_shortcut, self)
    self.find_again_backward_shortcut.activated.connect(backward)

connect_find_replace(handler, shortcut)

Create a keyboard shortcut and associate it with a handler for doing a find and replace.

Source code in mu/interface/main.py
def connect_find_replace(self, handler, shortcut):
    """
    Create a keyboard shortcut and associate it with a handler for doing
    a find and replace.
    """
    self.find_replace_shortcut = QShortcut(QKeySequence(shortcut), self)
    self.find_replace_shortcut.activated.connect(handler)

connect_tab_rename(handler, shortcut)

Connect the double-click event on a tab and the keyboard shortcut to the referenced handler (causing the Save As dialog).

Source code in mu/interface/main.py
def connect_tab_rename(self, handler, shortcut):
    """
    Connect the double-click event on a tab and the keyboard shortcut to
    the referenced handler (causing the Save As dialog).
    """
    self.tabs.shortcut = QShortcut(QKeySequence(shortcut), self)
    self.tabs.shortcut.activated.connect(handler)
    self.tabs.tabBarDoubleClicked.connect(handler)

connect_toggle_comments(handler, shortcut)

Create a keyboard shortcut and associate it with a handler for toggling comments on highlighted lines.

Source code in mu/interface/main.py
def connect_toggle_comments(self, handler, shortcut):
    """
    Create a keyboard shortcut and associate it with a handler for toggling
    comments on highlighted lines.
    """
    self.toggle_comments_shortcut = QShortcut(QKeySequence(shortcut), self)
    self.toggle_comments_shortcut.activated.connect(handler)

connect_zoom(widget)

Connects a referenced widget to the zoom related signals and sets the zoom of the widget to the current zoom level.

Source code in mu/interface/main.py
def connect_zoom(self, widget):
    """
    Connects a referenced widget to the zoom related signals and sets
    the zoom of the widget to the current zoom level.
    """
    self._zoom_in.connect(widget.set_zoom)
    self._zoom_out.connect(widget.set_zoom)
    widget.set_zoom(self.zooms[self.zoom_position])

copy_to_repl()

Copies currently selected text in the editor, into the active REPL widget and sets focus to the REPL widget. The final line pasted into the REPL waits for RETURN to be pressed by the user (this appears to be the default behaviour for pasting into the REPL widget).

Source code in mu/interface/main.py
def copy_to_repl(self):
    """
    Copies currently selected text in the editor, into the active REPL
    widget and sets focus to the REPL widget. The final line pasted into
    the REPL waits for RETURN to be pressed by the user (this appears to be
    the default behaviour for pasting into the REPL widget).
    """
    line_from, pos_from, line_to, pos_to = self.current_tab.getSelection()
    lines = self.current_tab.text().split("\n")
    if line_from == line_to:
        # Paste a fragment from an individual line.
        to_paste = lines[line_from][pos_from:pos_to]
    else:
        # Multi-line paste (paste all lines selected).
        selected = lines[line_from : line_to + 1]
        # Ensure the correct indentation.
        # If the first line starts with whitespace, work out the number of
        # spaces and deduct said number of whitespace from start of all
        # other lines. That's perhaps the best we can do.
        if selected and selected[0].startswith(" "):
            indent = 0
            for char in selected[0]:
                if char.isspace():
                    indent += 1
                else:
                    break
            selected = [line[indent:] for line in selected]
        to_paste = "\n".join(selected)
    logger.info("Pasting to REPL")
    logger.info("\n" + to_paste)
    clipboard = QApplication.clipboard()
    clipboard.setText(to_paste)
    if hasattr(self, "repl_pane") and self.repl_pane:
        self.repl_pane.paste()
        self.repl_pane.setFocus()
    elif hasattr(self, "process_runner") and self.process_runner:
        self.process_runner.paste()
        self.process_runner.setFocus()

focus_tab(tab)

Force focus on the referenced tab.

Source code in mu/interface/main.py
def focus_tab(self, tab):
    """
    Force focus on the referenced tab.
    """
    index = self.tabs.indexOf(tab)
    self.tabs.setCurrentIndex(index)
    tab.setFocus()

get_load_path(folder, extensions='*', allow_previous=True)

Displays a dialog for selecting a file to load. Returns the selected path. Defaults to start in the referenced folder unless a previous folder has been used and the allow_previous flag is True (the default behaviour)

Source code in mu/interface/main.py
def get_load_path(self, folder, extensions="*", allow_previous=True):
    """
    Displays a dialog for selecting a file to load. Returns the selected
    path. Defaults to start in the referenced folder unless a previous
    folder has been used and the allow_previous flag is True (the default
    behaviour)
    """
    if allow_previous:
        open_in = folder if self.previous_folder is None else self.previous_folder
    else:
        open_in = folder
    path, _ = QFileDialog.getOpenFileName(
        self.widget, "Open file", open_in, extensions
    )
    logger.debug("Getting load path: {}".format(path))
    if allow_previous:
        self.previous_folder = os.path.dirname(path)
    return path

get_save_path(folder)

Displays a dialog for selecting a file to save. Returns the selected path. Defaults to start in the referenced folder.

Source code in mu/interface/main.py
def get_save_path(self, folder):
    """
    Displays a dialog for selecting a file to save. Returns the selected
    path. Defaults to start in the referenced folder.
    """
    path, _ = QFileDialog.getSaveFileName(
        self.widget,
        "Save file",
        folder if self.previous_folder is None else self.previous_folder,
        "Python (*.py);;Other (*.*)",
        "Python (*.py)",
    )
    self.previous_folder = os.path.dirname(path)
    # Ensure there's a .py extension if none is provided by the user.
    # See issue #1571.
    name, ext = os.path.splitext(os.path.basename(path))
    if name and (not name.startswith(".")) and (not ext):
        # The file is not a . (dot) file and there's no extension, so add
        # .py as default.
        path += ".py"
    logger.debug("Getting save path: {}".format(path))
    return path

hide_device_selector()

Hides the device selector in the status bar

Source code in mu/interface/main.py
def hide_device_selector(self):
    """
    Hides the device selector in the status bar
    """
    self.status_bar.device_selector.setHidden(True)

highlight_text(target_text, forward=True)

Highlight the first match from the current position of the cursor in the current tab for the target_text. Returns True if there's a match.

Source code in mu/interface/main.py
def highlight_text(self, target_text, forward=True):
    """
    Highlight the first match from the current position of the cursor in
    the current tab for the target_text. Returns True if there's a match.
    """
    if self.current_tab:
        line = -1
        index = -1
        if not forward:
            # Workaround for `findFirst(forward=False)` not advancing
            # backwards: pass explicit line and index values.
            line, index, _el, _ei = self.current_tab.getSelection()
        return self.current_tab.findFirst(
            target_text,  # Text to find,
            False,  # Treat as regular expression
            True,  # Case sensitive search
            False,  # Whole word matches only
            True,  # Wrap search
            forward=forward,  # Forward search
            line=line,  # -1 starts at current position
            index=index,  # -1 starts at current position
        )
    else:
        return False

on_context_menu()

Called when a user right-clicks on an editor pane.

If the REPL is active AND there is selected text in the current editor pane, modify the default context menu to include a paste to REPL option. Otherwise, just display the default context menu.

Source code in mu/interface/main.py
def on_context_menu(self):
    """
    Called when a user right-clicks on an editor pane.

    If the REPL is active AND there is selected text in the current editor
    pane, modify the default context menu to include a paste to REPL
    option. Otherwise, just display the default context menu.
    """
    # Create a standard context menu.
    menu = self.current_tab.createStandardContextMenu()
    # Flag to indicate if there is a section of text highlighted.
    has_selection = any([x > -1 for x in self.current_tab.getSelection()])
    # Flag to indicate if the REPL pane is active.
    has_repl = hasattr(self, "repl") and self.repl
    # Flag to indicate if the Python process runner pane is active and in
    # interactive Python3 mode (pasting into other "standard" Python modes
    # doesn't make sense).
    has_runner = (
        hasattr(self, "process_runner")
        and self.process_runner
        and self.process_runner.is_interactive
    )
    if has_selection and (has_repl or has_runner):
        # Text selected with REPL/process context, so add the bespoke
        # "copy to repl" item to the standard context menu to handle this
        # situation.
        actions = menu.actions()
        copy_to_repl = QAction(_("Copy selected text to REPL"), menu)
        copy_to_repl.triggered.connect(self.copy_to_repl)
        menu.insertAction(actions[0], copy_to_repl)
        menu.insertSeparator(actions[0])
    # Display menu.
    menu.exec(QCursor.pos())

on_stdout_write(data)

Called when either a running script or the REPL write to STDOUT.

Source code in mu/interface/main.py
def on_stdout_write(self, data):
    """
    Called when either a running script or the REPL write to STDOUT.
    """
    self.data_received.emit(data)

open_directory_from_os(path)

Given the path to a directory, open the OS's built in filesystem explorer for that path. Works with Windows, OSX and Linux.

Source code in mu/interface/main.py
def open_directory_from_os(self, path):
    """
    Given the path to a directory, open the OS's built in filesystem
    explorer for that path. Works with Windows, OSX and Linux.
    """
    if sys.platform == "win32":
        # Windows
        os.startfile(path)
    elif sys.platform == "darwin":
        # OSX
        os.system('open "{}"'.format(path))
    else:
        # Assume freedesktop.org on unix-y.
        os.system('xdg-open "{}"'.format(path))

remove_debug_inspector()

Removes the debug inspector pane from the application.

Source code in mu/interface/main.py
def remove_debug_inspector(self):
    """
    Removes the debug inspector pane from the application.
    """
    if hasattr(self, "inspector") and self.inspector:
        width = self.debug_inspector.columnWidth
        self.debug_widths = width(0), width(1)
        self._inspector_area = self.dockWidgetArea(self.inspector)
        self.debug_inspector = None
        self.debug_model = None
        self.inspector.setParent(None)
        self.inspector.deleteLater()
        self.inspector = None

remove_plotter()

Removes the plotter pane from the application.

Source code in mu/interface/main.py
def remove_plotter(self):
    """
    Removes the plotter pane from the application.
    """
    if self.plotter:
        self._plotter_area = self.dockWidgetArea(self.plotter)
        self.plotter_pane = None
        self.plotter.setParent(None)
        self.plotter.deleteLater()
        self.plotter = None

remove_python_runner()

Removes the runner pane from the application.

Source code in mu/interface/main.py
def remove_python_runner(self):
    """
    Removes the runner pane from the application.
    """
    if hasattr(self, "runner") and self.runner:
        if self.process_runner.debugger:
            self._debugger_area = self.dockWidgetArea(self.runner)
        else:
            self._runner_area = self.dockWidgetArea(self.runner)
        self.process_runner = None
        self.runner.setParent(None)
        self.runner.deleteLater()
        self.runner = None

remove_repl()

Removes the REPL pane from the application.

Source code in mu/interface/main.py
def remove_repl(self):
    """
    Removes the REPL pane from the application.
    """
    if self.repl:
        self._repl_area = self.dockWidgetArea(self.repl)
        self.repl_pane = None
        self.repl.setParent(None)
        self.repl.deleteLater()
        self.repl = None

replace_text(target_text, replace, global_replace)

Given target_text, replace the first instance after the cursor with "replace". If global_replace is true, replace all instances of "target". Returns the number of times replacement has occurred.

Source code in mu/interface/main.py
def replace_text(self, target_text, replace, global_replace):
    """
    Given target_text, replace the first instance after the cursor with
    "replace". If global_replace is true, replace all instances of
    "target". Returns the number of times replacement has occurred.
    """
    if not self.current_tab:
        return 0
    if global_replace:
        counter = 0
        found = self.current_tab.findFirst(
            target_text, False, True, False, False, line=0, index=0
        )
        if found:
            counter += 1
            self.current_tab.replace(replace)
            while self.current_tab.findNext():
                self.current_tab.replace(replace)
                counter += 1
        return counter
    else:
        found = self.current_tab.findFirst(target_text, False, True, False, True)
        if found:
            self.current_tab.replace(replace)
            return 1
        else:
            return 0

reset_annotations()

Resets the state of annotations on the current tab.

Source code in mu/interface/main.py
def reset_annotations(self):
    """
    Resets the state of annotations on the current tab.
    """
    self.current_tab.reset_annotations()

resizeEvent(resizeEvent)

Respond to window getting too small for the button bar to fit well.

Source code in mu/interface/main.py
def resizeEvent(self, resizeEvent):
    """
    Respond to window getting too small for the button bar to fit well.
    """
    size = resizeEvent.size()
    self.button_bar.set_responsive_mode(size.width(), size.height())

screen_size()

Returns an (width, height) tuple with the screen geometry.

Source code in mu/interface/main.py
def screen_size(self):
    """
    Returns an (width, height) tuple with the screen geometry.
    """
    screen = QApplication.primaryScreen().geometry()
    return screen.width(), screen.height()

select_mode(modes, current_mode)

Display the mode selector dialog and return the result.

Source code in mu/interface/main.py
def select_mode(self, modes, current_mode):
    """
    Display the mode selector dialog and return the result.
    """
    mode_select = ModeSelector(self)
    mode_select.setup(modes, current_mode)
    mode_select.exec()
    try:
        return mode_select.get_mode()
    except Exception:
        return None

set_checker_icon(icon)

Set the status icon to use on the check button

Source code in mu/interface/main.py
def set_checker_icon(self, icon):
    """
    Set the status icon to use on the check button
    """
    self.button_bar.slots["check"].setIcon(load_icon(icon))
    timer = QTimer()

    @timer.timeout.connect
    def reset():
        self.button_bar.slots["check"].setIcon(load_icon("check"))
        timer.stop()

    timer.start(500)

set_read_only(is_readonly)

Set all tabs read-only.

Source code in mu/interface/main.py
def set_read_only(self, is_readonly):
    """
    Set all tabs read-only.
    """
    self.read_only_tabs = is_readonly
    for tab in self.widgets:
        tab.setReadOnly(is_readonly)

set_theme(theme)

Sets the theme for the REPL and editor tabs.

Source code in mu/interface/main.py
def set_theme(self, theme):
    """
    Sets the theme for the REPL and editor tabs.
    """
    self.theme = theme
    self.load_theme.emit(theme)
    if theme == "contrast":
        new_theme = ContrastTheme
        new_icon = "theme_day"
    elif theme == "night":
        new_theme = NightTheme
        new_icon = "theme_contrast"
    else:
        new_theme = DayTheme
        new_icon = "theme"
    for widget in self.widgets:
        widget.set_theme(new_theme)
    self.button_bar.slots["theme"].setIcon(load_icon(new_icon))
    if hasattr(self, "repl") and self.repl:
        self.repl_pane.set_theme(theme)
    if hasattr(self, "plotter") and self.plotter:
        self.plotter_pane.set_theme(theme)
    if hasattr(self, "runner") and self.runner:
        self.process_runner.set_theme(theme)

set_timer(duration, callback)

Set a repeating timer to call "callback" every "duration" seconds.

Source code in mu/interface/main.py
def set_timer(self, duration, callback):
    """
    Set a repeating timer to call "callback" every "duration" seconds.
    """
    self.timer = QTimer()
    self.timer.timeout.connect(callback)
    self.timer.start(duration * 1000)  # Measured in milliseconds.

set_usb_checker(duration, callback)

Sets up a timer that polls for USB changes via the "callback" every "duration" seconds.

Source code in mu/interface/main.py
def set_usb_checker(self, duration, callback):
    """
    Sets up a timer that polls for USB changes via the "callback" every
    "duration" seconds.
    """
    self.usb_checker = QTimer()
    self.usb_checker.timeout.connect(callback)
    self.usb_checker.start(duration * 1000)

set_zoom()

Sets the zoom to current zoom_position level.

Source code in mu/interface/main.py
def set_zoom(self):
    """
    Sets the zoom to current zoom_position level.
    """
    self._zoom_in.emit(self.zooms[self.zoom_position])

setup(breakpoint_toggle, theme)

Sets up the window.

Defines the various attributes of the window and defines how the user interface is laid out.

Source code in mu/interface/main.py
def setup(self, breakpoint_toggle, theme):
    """
    Sets up the window.

    Defines the various attributes of the window and defines how the user
    interface is laid out.
    """
    self.theme = theme
    self.breakpoint_toggle = breakpoint_toggle
    # Give the window a default icon, title and minimum size.
    self.setWindowIcon(load_icon(self.icon))
    self.update_title()
    self.read_only_tabs = False
    screen_width, screen_height = self.screen_size()
    self.setMinimumSize(
        min(screen_width // 2, MIN_WINDOW_WIDTH),
        min(screen_height // 2, MIN_WINDOW_HEIGHT),
    )
    self.setTabPosition(
        Qt.DockWidgetArea.AllDockWidgetAreas, QTabWidget.TabPosition.North
    )
    self.widget = QWidget()
    widget_layout = QVBoxLayout()
    self.widget.setLayout(widget_layout)
    self.button_bar = ButtonBar(self.widget)
    self.tabs = FileTabs()
    self.setCentralWidget(self.tabs)
    self.status_bar = StatusBar(parent=self)
    self.setStatusBar(self.status_bar)
    self.addToolBar(self.button_bar)
    self.show()

show_admin(log, settings, packages, mode, device_list)

Display the administrative dialog with referenced content of the log and settings. Return a dictionary of the settings that may have been changed by the admin dialog.

Source code in mu/interface/main.py
def show_admin(self, log, settings, packages, mode, device_list):
    """
    Display the administrative dialog with referenced content of the log
    and settings. Return a dictionary of the settings that may have been
    changed by the admin dialog.
    """
    admin_box = AdminDialog(self)
    admin_box.setup(log, settings, packages, mode, device_list)
    result = admin_box.exec()
    if result:
        return admin_box.settings()
    else:
        return {}

show_annotations()

Show the annotations added to the current tab.

Source code in mu/interface/main.py
def show_annotations(self):
    """
    Show the annotations added to the current tab.
    """
    self.current_tab.show_annotations()

show_confirmation(message, information=None, icon=None)

Displays a modal message to the user to which they need to confirm or cancel.

If information is passed in this will be set as the additional informative text in the modal dialog.

Since this mechanism will be used mainly for warning users that something is awry the default icon is set to "Warning". It's possible to override the icon to one of the following settings: NoIcon, Question, Information, Warning or Critical.

Source code in mu/interface/main.py
def show_confirmation(self, message, information=None, icon=None):
    """
    Displays a modal message to the user to which they need to confirm or
    cancel.

    If information is passed in this will be set as the additional
    informative text in the modal dialog.

    Since this mechanism will be used mainly for warning users that
    something is awry the default icon is set to "Warning". It's possible
    to override the icon to one of the following settings: NoIcon,
    Question, Information, Warning or Critical.
    """
    message_box = QMessageBox(self)
    message_box.setText(message)
    message_box.setWindowTitle(_("Nu"))
    if information:
        message_box.setInformativeText(information)
    if icon and hasattr(QMessageBox.Icon, icon):
        message_box.setIcon(getattr(QMessageBox.Icon, icon))
    else:
        message_box.setIcon(QMessageBox.Icon.Warning)
    message_box.setStandardButtons(
        QMessageBox.StandardButton.Cancel | QMessageBox.StandardButton.Ok
    )
    message_box.setDefaultButton(QMessageBox.StandardButton.Cancel)
    logger.debug(message)
    logger.debug(information)
    return message_box.exec()

show_device_selector()

Reveals the device selector in the status bar

Source code in mu/interface/main.py
def show_device_selector(self):
    """
    Reveals the device selector in the status bar
    """
    self.status_bar.device_selector.setHidden(False)

show_find_replace(find, replace, global_replace)

Display the find/replace dialog. If the dialog's OK button was clicked return a tuple containing the find term, replace term and global replace flag.

Source code in mu/interface/main.py
def show_find_replace(self, find, replace, global_replace):
    """
    Display the find/replace dialog. If the dialog's OK button was clicked
    return a tuple containing the find term, replace term and global
    replace flag.
    """
    finder = FindReplaceDialog(self)
    finder.setup(find, replace, global_replace)
    if finder.exec():
        return (finder.find(), finder.replace(), finder.replace_flag())

show_message(message, information=None, icon=None)

Displays a modal message to the user.

If information is passed in this will be set as the additional informative text in the modal dialog.

Since this mechanism will be used mainly for warning users that something is awry the default icon is set to "Warning". It's possible to override the icon to one of the following settings: NoIcon, Question, Information, Warning or Critical.

Source code in mu/interface/main.py
def show_message(self, message, information=None, icon=None):
    """
    Displays a modal message to the user.

    If information is passed in this will be set as the additional
    informative text in the modal dialog.

    Since this mechanism will be used mainly for warning users that
    something is awry the default icon is set to "Warning". It's possible
    to override the icon to one of the following settings: NoIcon,
    Question, Information, Warning or Critical.
    """
    message_box = QMessageBox(self)
    message_box.setText(message)
    message_box.setWindowTitle("Nu")
    if information:
        message_box.setInformativeText(information)
    if icon and hasattr(QMessageBox.Icon, icon):
        message_box.setIcon(getattr(QMessageBox.Icon, icon))
    else:
        message_box.setIcon(QMessageBox.Icon.Warning)
    logger.debug(message)
    logger.debug(information)
    message_box.exec()

size_window(x=None, y=None, w=None, h=None)

Makes the editor 80% of the width*height of the screen and centres it when none of x, y, w and h is passed in; otherwise uses the passed in values to position and size the editor window.

If the X or Y value will be off the screen, these are reset to None (thus stopping the window being drawn in a hard-to-reach place). See issue #1613 for context.

Source code in mu/interface/main.py
def size_window(self, x=None, y=None, w=None, h=None):
    """
    Makes the editor 80% of the width*height of the screen and centres it
    when none of x, y, w and h is passed in; otherwise uses the passed in
    values to position and size the editor window.

    If the X or Y value will be off the screen, these are reset to None
    (thus stopping the window being drawn in a hard-to-reach place). See
    issue #1613 for context.
    """
    screen_width, screen_height = self.screen_size()
    w = int(screen_width * 0.8) if w is None else w
    h = int(screen_height * 0.8) if h is None else h
    self.resize(w, h)
    size = self.geometry()
    # Ensure the window isn't added off the screen.
    if x and (x <= 0 or x > screen_width):
        x = None
    if y and (y <= 0 or y > screen_height):
        y = None
    x = (screen_width - size.width()) // 2 if x is None else x
    y = (screen_height - size.height()) // 2 if y is None else y
    self.move(x, y)

stop_timer()

Stop the repeating timer.

Source code in mu/interface/main.py
def stop_timer(self):
    """
    Stop the repeating timer.
    """
    if self.timer:
        self.timer.stop()
        self.timer = None

sync_packages(to_remove, to_add)

Display a modal dialog that indicates the status of the add/remove package management operation.

Source code in mu/interface/main.py
def sync_packages(self, to_remove, to_add):
    """
    Display a modal dialog that indicates the status of the add/remove
    package management operation.
    """
    package_box = PackageDialog(self)
    package_box.setup(to_remove, to_add)
    package_box.exec()

toggle_comments()

Toggle comments on/off for all selected line in the currently active tab.

Source code in mu/interface/main.py
def toggle_comments(self):
    """
    Toggle comments on/off for all selected line in the currently active
    tab.
    """
    if self.current_tab:
        self.current_tab.toggle_comments()

update_debug_inspector(locals_dict)

Given the contents of a dict representation of the locals in the current stack frame, update the debug inspector with the new values.

Source code in mu/interface/main.py
def update_debug_inspector(self, locals_dict):
    """
    Given the contents of a dict representation of the locals in the
    current stack frame, update the debug inspector with the new values.
    """
    excluded_names = ["__builtins__", "__debug_code__", "__debug_script__"]
    names = sorted([x for x in locals_dict if x not in excluded_names])

    # Remove rows so we keep the same column layouts if manually set
    while self.debug_model.rowCount() > 0:
        self.debug_model.removeRow(0)
    for name in names:
        item_to_expand = None
        try:
            # DANGER!
            val = eval(locals_dict[name])
        except Exception:
            val = None
        if isinstance(val, list):
            # Show a list consisting of rows of position/value
            list_item = DebugInspectorItem(name)
            item_to_expand = list_item
            for i, i_val in enumerate(val):
                list_item.appendRow(
                    [
                        DebugInspectorItem(str(i)),
                        DebugInspectorItem(repr(i_val)),
                    ]
                )
            self.debug_model.appendRow(
                [
                    list_item,
                    DebugInspectorItem(_("(A list of {} items.)").format(len(val))),
                ]
            )
        elif isinstance(val, dict):
            # Show a dict consisting of rows of key/value pairs.
            dict_item = DebugInspectorItem(name)
            item_to_expand = dict_item
            for k, k_val in val.items():
                dict_item.appendRow(
                    [
                        DebugInspectorItem(repr(k)),
                        DebugInspectorItem(repr(k_val)),
                    ]
                )
            self.debug_model.appendRow(
                [
                    dict_item,
                    DebugInspectorItem(_("(A dict of {} items.)").format(len(val))),
                ]
            )
        else:
            self.debug_model.appendRow(
                [
                    DebugInspectorItem(name),
                    DebugInspectorItem(locals_dict[name]),
                ]
            )
        # Expand dicts/list with names matching old expanded entries
        if (
            hasattr(self, "debug_inspector")
            and name in self.debug_inspector.expanded_dicts
            and item_to_expand is not None
        ):
            self.debug_inspector.expand(
                self.debug_model.indexFromItem(item_to_expand)
            )

update_title(filename=None)

Updates the title bar of the application. If a filename (representing the name of the file currently the focus of the editor) is supplied, append it to the end of the title.

Source code in mu/interface/main.py
def update_title(self, filename=None):
    """
    Updates the title bar of the application. If a filename (representing
    the name of the file currently the focus of the editor) is supplied,
    append it to the end of the title.
    """
    title = self.title
    if filename:
        title += " - " + filename
    self.setWindowTitle(title)

wheelEvent(event)

Trap a CTRL-scroll event so the user is able to zoom in and out.

Source code in mu/interface/main.py
def wheelEvent(self, event):
    """
    Trap a CTRL-scroll event so the user is able to zoom in and out.
    """
    modifiers = QApplication.keyboardModifiers()
    if modifiers == Qt.KeyboardModifier.ControlModifier:
        zoom = event.angleDelta().y() > 0
        if zoom:
            self.zoom_in()
        else:
            self.zoom_out()
        event.ignore()

zoom_in()

Handles zooming in.

Source code in mu/interface/main.py
def zoom_in(self):
    """
    Handles zooming in.
    """
    self.zoom_position = min(self.zoom_position + 1, len(self.zooms) - 1)
    self._zoom_in.emit(self.zooms[self.zoom_position])

zoom_out()

Handles zooming out.

Source code in mu/interface/main.py
def zoom_out(self):
    """
    Handles zooming out.
    """
    self.zoom_position = max(self.zoom_position - 1, 0)
    self._zoom_out.emit(self.zooms[self.zoom_position])

mu.interface.dialogs

Bespoke modal dialogs required by Mu.

mu.interface.dialogs

UI related code for dialogs used by Mu.

Copyright (c) 2015-2017 Nicholas H.Tollervey and others (see the AUTHORS file).

This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/.

AdminDialog

Bases: QDialog

Displays administrative related information and settings (logs, environment variables, third party packages etc...).

Source code in mu/interface/dialogs.py
class AdminDialog(QDialog):
    """
    Displays administrative related information and settings (logs, environment
    variables, third party packages etc...).
    """

    def __init__(self, parent=None):
        super().__init__(parent)
        self.package_widget = None
        self.envar_widget = None

    def setup(self, log, settings, packages, mode, device_list):
        self.setMinimumSize(600, 400)
        self.setWindowTitle(_("Nu Administration"))
        widget_layout = QVBoxLayout()
        self.setLayout(widget_layout)
        self.tabs = QTabWidget()
        widget_layout.addWidget(self.tabs)
        button_box = QDialogButtonBox(
            QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
        )
        button_box.accepted.connect(self.accept)
        button_box.rejected.connect(self.reject)
        widget_layout.addWidget(button_box)
        # Tabs
        self.log_widget = LogWidget(self)
        self.log_widget.setup(log)
        self.tabs.addTab(self.log_widget, _("Current Log"))
        if mode.short_name in ["python", "pygamezero"]:
            self.envar_widget = EnvironmentVariablesWidget(self)
            self.envar_widget.setup(settings.get("envars", ""))
            self.tabs.addTab(self.envar_widget, _("Python3 Environment"))
        if mode.short_name in ["python", "pygamezero"]:
            self.package_widget = PackagesWidget(self)
            self.package_widget.setup(packages)
            self.tabs.addTab(self.package_widget, _("Third Party Packages"))
        # Configure local.
        self.locale_widget = LocaleWidget(self)
        self.locale_widget.setup(settings.get("locale"))
        self.tabs.addTab(
            self.locale_widget, load_icon("language.svg"), _("Select Language")
        )
        self.log_widget.log_text_area.setFocus()

    def settings(self):
        """
        Return a dictionary representation of the raw settings information
        generated by this dialog. Such settings will need to be processed /
        checked in the "logic" layer of Mu.
        """
        settings = {}
        if self.envar_widget:
            settings["envars"] = self.envar_widget.text_area.toPlainText()
        if self.package_widget:
            settings["packages"] = self.package_widget.text_area.toPlainText()
        settings["locale"] = self.locale_widget.get_locale()
        return settings

settings()

Return a dictionary representation of the raw settings information generated by this dialog. Such settings will need to be processed / checked in the "logic" layer of Mu.

Source code in mu/interface/dialogs.py
def settings(self):
    """
    Return a dictionary representation of the raw settings information
    generated by this dialog. Such settings will need to be processed /
    checked in the "logic" layer of Mu.
    """
    settings = {}
    if self.envar_widget:
        settings["envars"] = self.envar_widget.text_area.toPlainText()
    if self.package_widget:
        settings["packages"] = self.package_widget.text_area.toPlainText()
    settings["locale"] = self.locale_widget.get_locale()
    return settings

CrashDialog

Bases: QDialog

Shown when Nu hits an error it can't recover from. It shows the crash details and lets the user copy them (e.g. into a bug report); nothing is sent anywhere.

Source code in mu/interface/dialogs.py
class CrashDialog(QDialog):
    """
    Shown when Nu hits an error it can't recover from. It shows the crash
    details and lets the user copy them (e.g. into a bug report); nothing is
    sent anywhere.
    """

    ISSUES_URL = "https://github.com/kovirlabs/nu/issues"

    def __init__(self, details, parent=None):
        super().__init__(parent)
        self.details = details
        self.setWindowTitle(_("Nu has crashed"))
        self.setMinimumSize(600, 400)
        layout = QVBoxLayout(self)
        label = QLabel(
            _(
                "Sorry, Nu hit an error it couldn't recover from and has to "
                "close. Nothing has been sent anywhere.<br><br>To report it, "
                'copy the details below into a new issue at <a href="{url}">'
                "{url}</a>."
            ).format(url=self.ISSUES_URL)
        )
        label.setWordWrap(True)
        label.setOpenExternalLinks(True)
        layout.addWidget(label)
        self.text_area = QPlainTextEdit()
        self.text_area.setReadOnly(True)
        self.text_area.setPlainText(details)
        layout.addWidget(self.text_area)
        button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
        self.copy_button = QPushButton(_("Copy to clipboard"))
        button_box.addButton(self.copy_button, QDialogButtonBox.ButtonRole.ActionRole)
        self.copy_button.clicked.connect(self.copy_details)
        button_box.rejected.connect(self.reject)
        layout.addWidget(button_box)

    def copy_details(self):
        """Put the crash details on the clipboard."""
        QApplication.clipboard().setText(self.details)
        self.copy_button.setText(_("Copied"))

copy_details()

Put the crash details on the clipboard.

Source code in mu/interface/dialogs.py
def copy_details(self):
    """Put the crash details on the clipboard."""
    QApplication.clipboard().setText(self.details)
    self.copy_button.setText(_("Copied"))

EnvironmentVariablesWidget

Bases: QWidget

Used for editing and displaying environment variables used with Python 3 mode.

Source code in mu/interface/dialogs.py
class EnvironmentVariablesWidget(QWidget):
    """
    Used for editing and displaying environment variables used with Python 3
    mode.
    """

    def setup(self, envars):
        widget_layout = QVBoxLayout()
        self.setLayout(widget_layout)
        label = QLabel(
            _(
                "The environment variables shown below will be "
                "set each time you run a Python 3 script.\n\n"
                "Each separate environment variable should be on a "
                "new line and of the form:\nNAME=VALUE"
            )
        )
        label.setWordWrap(True)
        widget_layout.addWidget(label)
        self.text_area = QPlainTextEdit()
        self.text_area.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap)
        self.text_area.setPlainText(envars)
        widget_layout.addWidget(self.text_area)

FindReplaceDialog

Bases: QDialog

Display a dialog for getting:

  • A term to find,
  • An optional value to replace the search term,
  • A flag to indicate if the user wishes to replace all.
Source code in mu/interface/dialogs.py
class FindReplaceDialog(QDialog):
    """
    Display a dialog for getting:

    * A term to find,
    * An optional value to replace the search term,
    * A flag to indicate if the user wishes to replace all.
    """

    def __init__(self, parent=None):
        super().__init__(parent)

    def setup(self, find=None, replace=None, replace_flag=False):
        self.setMinimumSize(600, 200)
        self.setWindowTitle(_("Find / Replace"))
        widget_layout = QVBoxLayout()
        self.setLayout(widget_layout)
        # Find.
        find_label = QLabel(_("Find:"))
        self.find_term = QLineEdit()
        self.find_term.setText(find)
        self.find_term.selectAll()
        widget_layout.addWidget(find_label)
        widget_layout.addWidget(self.find_term)
        # Replace
        replace_label = QLabel(_("Replace (optional):"))
        self.replace_term = QLineEdit()
        self.replace_term.setText(replace)
        widget_layout.addWidget(replace_label)
        widget_layout.addWidget(self.replace_term)
        # Global replace.
        self.replace_all_flag = QCheckBox(_("Replace all?"))
        self.replace_all_flag.setChecked(replace_flag)
        widget_layout.addWidget(self.replace_all_flag)
        button_box = QDialogButtonBox(
            QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
        )
        button_box.accepted.connect(self.accept)
        button_box.rejected.connect(self.reject)
        widget_layout.addWidget(button_box)

    def find(self):
        """
        Return the value the user entered to find.
        """
        return self.find_term.text()

    def replace(self):
        """
        Return the value the user entered for replace.
        """
        return self.replace_term.text()

    def replace_flag(self):
        """
        Return the value of the global replace flag.
        """
        return self.replace_all_flag.isChecked()

find()

Return the value the user entered to find.

Source code in mu/interface/dialogs.py
def find(self):
    """
    Return the value the user entered to find.
    """
    return self.find_term.text()

replace()

Return the value the user entered for replace.

Source code in mu/interface/dialogs.py
def replace(self):
    """
    Return the value the user entered for replace.
    """
    return self.replace_term.text()

replace_flag()

Return the value of the global replace flag.

Source code in mu/interface/dialogs.py
def replace_flag(self):
    """
    Return the value of the global replace flag.
    """
    return self.replace_all_flag.isChecked()

LocaleWidget

Bases: QWidget

Used for manually setting the locale (and thus the language) used by Mu.

Source code in mu/interface/dialogs.py
class LocaleWidget(QWidget):
    """
    Used for manually setting the locale (and thus the language) used by Mu.
    """

    LANGUAGES = {
        _("Automatically detect"): "",
        "English": "en",
        "Deutsch": "de_DE",
        "Español": "es",
        "Français": "fr",
        "日本語": "ja",
        "Nederlands": "nl",
        "Polski": "pl",
        "Português (Br)": "pt_BR",
        "Português (Pt)": "pt_PT",
        "русский язык": "ru_RU",
        "Slovenský": "sk_SK",
        "Svenska": "sv",
        "tiếng Việt": "vi",
        "中文": "zh_CN",
    }

    def setup(self, locale):
        widget_layout = QVBoxLayout()
        self.setLayout(widget_layout)
        self.drop_down = QComboBox()
        for k, v in self.LANGUAGES.items():
            self.drop_down.addItem(k, v)
        index = self.drop_down.findData(locale)
        if index > -1:
            self.drop_down.setCurrentIndex(index)
        label = QLabel(
            _(
                "Please select the language for Nu's user interface from the "
                "choices listed below. <strong>Restart Nu for these changes "
                "to take effect.</strong>"
            )
        )
        label.setWordWrap(True)
        widget_layout.addWidget(label)
        widget_layout.addWidget(self.drop_down)
        widget_layout.addStretch()

    def get_locale(self):
        """
        Return the user-selected language code.
        """
        return self.LANGUAGES.get(self.drop_down.currentText(), "")

get_locale()

Return the user-selected language code.

Source code in mu/interface/dialogs.py
def get_locale(self):
    """
    Return the user-selected language code.
    """
    return self.LANGUAGES.get(self.drop_down.currentText(), "")

LogWidget

Bases: QWidget

Used to display Mu's logs.

Source code in mu/interface/dialogs.py
class LogWidget(QWidget):
    """
    Used to display Mu's logs.
    """

    def setup(self, log):
        widget_layout = QVBoxLayout()
        self.setLayout(widget_layout)
        label = QLabel(
            _(
                "When reporting a bug, copy and paste the content of "
                "the following log file."
            )
        )
        label.setWordWrap(True)
        widget_layout.addWidget(label)
        self.log_text_area = QPlainTextEdit()
        self.log_text_area.setReadOnly(True)
        self.log_text_area.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap)
        self.log_text_area.setPlainText(log)
        widget_layout.addWidget(self.log_text_area)

ModeItem

Bases: QListWidgetItem

Represents an available mode listed for selection.

Source code in mu/interface/dialogs.py
class ModeItem(QListWidgetItem):
    """
    Represents an available mode listed for selection.
    """

    def __init__(self, name, description, icon, parent=None):
        super().__init__(parent)
        self.name = name
        self.description = description
        self.icon = icon
        text = "{}\n{}".format(name, description)
        self.setText(text)
        self.setIcon(load_icon(self.icon))

ModeSelector

Bases: QDialog

Defines a UI for selecting the mode for Mu.

Source code in mu/interface/dialogs.py
class ModeSelector(QDialog):
    """
    Defines a UI for selecting the mode for Mu.
    """

    def __init__(self, parent=None):
        super().__init__(parent)

    def setup(self, modes, current_mode):
        self.setMinimumSize(600, 400)
        self.setWindowTitle(_("Select Mode"))
        widget_layout = QVBoxLayout()
        label = QLabel(
            _(
                'Please select the desired mode then click "OK". '
                'Otherwise, click "Cancel".'
            )
        )
        label.setWordWrap(True)
        widget_layout.addWidget(label)
        self.setLayout(widget_layout)
        self.mode_list = QListWidget()
        self.mode_list.itemDoubleClicked.connect(self.select_and_accept)
        widget_layout.addWidget(self.mode_list)
        self.mode_list.setIconSize(QSize(48, 48))
        for name, item in modes.items():
            if not item.is_debugger:
                litem = ModeItem(item.name, item.description, item.icon, self.mode_list)
                if item.icon == current_mode:
                    self.mode_list.setCurrentItem(litem)
        self.mode_list.sortItems()
        instructions = QLabel(
            _(
                "Change mode at any time by clicking "
                'the "Mode" button containing Nu\'s logo.'
            )
        )
        instructions.setWordWrap(True)
        widget_layout.addWidget(instructions)
        button_box = QDialogButtonBox(
            QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
        )
        button_box.accepted.connect(self.accept)
        button_box.rejected.connect(self.reject)
        widget_layout.addWidget(button_box)

    def select_and_accept(self):
        """
        Handler for when an item is double-clicked.
        """
        self.accept()

    def get_mode(self):
        """
        Return details of the newly selected mode.
        """
        if self.result() == QDialog.DialogCode.Accepted:
            return self.mode_list.currentItem().icon
        else:
            raise RuntimeError("Mode change cancelled.")

get_mode()

Return details of the newly selected mode.

Source code in mu/interface/dialogs.py
def get_mode(self):
    """
    Return details of the newly selected mode.
    """
    if self.result() == QDialog.DialogCode.Accepted:
        return self.mode_list.currentItem().icon
    else:
        raise RuntimeError("Mode change cancelled.")

select_and_accept()

Handler for when an item is double-clicked.

Source code in mu/interface/dialogs.py
def select_and_accept(self):
    """
    Handler for when an item is double-clicked.
    """
    self.accept()

PackageDialog

Bases: QDialog

Display the output of the pip commands needed to remove or install packages.

Because the QProcess mechanism we're using is asynchronous, we have to manage the pip requests via pip_queue. When one request is signalled as finished we start the next.

Source code in mu/interface/dialogs.py
class PackageDialog(QDialog):
    """
    Display the output of the pip commands needed to remove or install
    packages.

    Because the QProcess mechanism we're using is asynchronous, we have to
    manage the pip requests via `pip_queue`. When one request is signalled
    as finished we start the next.
    """

    def __init__(self, parent=None):
        super().__init__(parent)
        self.pip_queue = []

    def setup(self, to_remove, to_add):
        """
        Create the UI for the dialog.
        """
        # Basic layout.
        self.setMinimumSize(600, 400)
        self.setWindowTitle(_("Third Party Package Status"))
        widget_layout = QVBoxLayout()
        self.setLayout(widget_layout)
        # Text area for pip output.
        self.text_area = QPlainTextEdit()
        self.text_area.setReadOnly(True)
        self.text_area.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap)
        widget_layout.addWidget(self.text_area)
        # Buttons.
        self.button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok)
        self.button_box.button(QDialogButtonBox.StandardButton.Ok).setEnabled(False)
        self.button_box.accepted.connect(self.accept)
        widget_layout.addWidget(self.button_box)

        #
        # Set up the commands to be issues to pip. Since we'll be popping
        # from the list (as LIFO) we'll add the installs first so the
        # removes are the first to happen
        #
        if to_add:
            self.pip_queue.append(("install", to_add))
        if to_remove:
            self.pip_queue.append(("remove", to_remove))
        QTimer.singleShot(2, self.next_pip_command)

    def next_pip_command(self):
        """
        Run the next pip command, finishing if there is none.
        """
        if self.pip_queue:
            command, packages = self.pip_queue.pop()
            self.run_pip(command, packages)
        else:
            self.finish()

    def finish(self):
        """
        Set the UI to a valid end state.
        """
        self.text_area.appendPlainText("\nFINISHED")
        self.button_box.button(QDialogButtonBox.StandardButton.Ok).setEnabled(True)

    def run_pip(self, command, packages):
        """
        Run a pip command in a subprocess and pipe the output to the dialog's
        text area.
        """
        if command == "remove":
            pip_fn = venv.remove_user_packages
        elif command == "install":
            pip_fn = venv.install_user_packages
        else:
            raise RuntimeError("Invalid pip command: %s %s" % (command, packages))
        pip_fn(
            packages,
            slots=venv.Slots(
                output=self.text_area.appendPlainText,
                finished=self.next_pip_command,
            ),
        )

finish()

Set the UI to a valid end state.

Source code in mu/interface/dialogs.py
def finish(self):
    """
    Set the UI to a valid end state.
    """
    self.text_area.appendPlainText("\nFINISHED")
    self.button_box.button(QDialogButtonBox.StandardButton.Ok).setEnabled(True)

next_pip_command()

Run the next pip command, finishing if there is none.

Source code in mu/interface/dialogs.py
def next_pip_command(self):
    """
    Run the next pip command, finishing if there is none.
    """
    if self.pip_queue:
        command, packages = self.pip_queue.pop()
        self.run_pip(command, packages)
    else:
        self.finish()

run_pip(command, packages)

Run a pip command in a subprocess and pipe the output to the dialog's text area.

Source code in mu/interface/dialogs.py
def run_pip(self, command, packages):
    """
    Run a pip command in a subprocess and pipe the output to the dialog's
    text area.
    """
    if command == "remove":
        pip_fn = venv.remove_user_packages
    elif command == "install":
        pip_fn = venv.install_user_packages
    else:
        raise RuntimeError("Invalid pip command: %s %s" % (command, packages))
    pip_fn(
        packages,
        slots=venv.Slots(
            output=self.text_area.appendPlainText,
            finished=self.next_pip_command,
        ),
    )

setup(to_remove, to_add)

Create the UI for the dialog.

Source code in mu/interface/dialogs.py
def setup(self, to_remove, to_add):
    """
    Create the UI for the dialog.
    """
    # Basic layout.
    self.setMinimumSize(600, 400)
    self.setWindowTitle(_("Third Party Package Status"))
    widget_layout = QVBoxLayout()
    self.setLayout(widget_layout)
    # Text area for pip output.
    self.text_area = QPlainTextEdit()
    self.text_area.setReadOnly(True)
    self.text_area.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap)
    widget_layout.addWidget(self.text_area)
    # Buttons.
    self.button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok)
    self.button_box.button(QDialogButtonBox.StandardButton.Ok).setEnabled(False)
    self.button_box.accepted.connect(self.accept)
    widget_layout.addWidget(self.button_box)

    #
    # Set up the commands to be issues to pip. Since we'll be popping
    # from the list (as LIFO) we'll add the installs first so the
    # removes are the first to happen
    #
    if to_add:
        self.pip_queue.append(("install", to_add))
    if to_remove:
        self.pip_queue.append(("remove", to_remove))
    QTimer.singleShot(2, self.next_pip_command)

PackagesWidget

Bases: QWidget

Used for editing and displaying 3rd party packages installed via pip to be used with Python 3 mode.

Source code in mu/interface/dialogs.py
class PackagesWidget(QWidget):
    """
    Used for editing and displaying 3rd party packages installed via pip to be
    used with Python 3 mode.
    """

    def setup(self, packages):
        widget_layout = QVBoxLayout()
        self.setLayout(widget_layout)
        self.text_area = QPlainTextEdit()
        self.text_area.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap)
        label = QLabel(
            _(
                "The packages shown below will be available to "
                "import in Python 3 mode. Delete a package from "
                "the list to remove its availability.\n\n"
                "Each separate package name should be on a new "
                "line. Packages are installed from PyPI "
                "(see: https://pypi.org/)."
            )
        )
        label.setWordWrap(True)
        widget_layout.addWidget(label)
        self.text_area.setPlainText(packages)
        widget_layout.addWidget(self.text_area)

mu.interface.editor

Contains the customised Scintilla based editor used for textual display and entry.

mu.interface.editor

UI related capabilities for the text editor widget embedded in each tab in Mu.

Copyright (c) 2015-2017 Nicholas H.Tollervey and others (see the AUTHORS file).

This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/.

CssLexer

Bases: QsciLexerCSS

Fixes problems with comments in CSS.

Source code in mu/interface/editor.py
class CssLexer(QsciLexerCSS):
    """
    Fixes problems with comments in CSS.
    """

    def description(self, style):
        """
        Ensures "Comment" is returned when the lexer encounters a comment (this
        is due to a bug in the base class, for which this is a work around).
        """
        if style == QsciLexerCSS.Comment:
            return "Comment"
        return super().description(style)

description(style)

Ensures "Comment" is returned when the lexer encounters a comment (this is due to a bug in the base class, for which this is a work around).

Source code in mu/interface/editor.py
def description(self, style):
    """
    Ensures "Comment" is returned when the lexer encounters a comment (this
    is due to a bug in the base class, for which this is a work around).
    """
    if style == QsciLexerCSS.Comment:
        return "Comment"
    return super().description(style)

EditorPane

Bases: QsciScintilla

Represents the text editor.

Source code in mu/interface/editor.py
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
class EditorPane(QsciScintilla):
    """
    Represents the text editor.
    """

    # Signal fired when a script or hex is dropped on this editor.
    open_file = pyqtSignal(str)
    # Signal fired when a context menu is requested.
    context_menu = pyqtSignal()

    def __init__(self, path, text, newline=NEWLINE):
        super().__init__()
        self.setUtf8(True)
        self.path = path
        self.setText(text)
        self.newline = newline
        self.check_indicators = {  # IDs are arbitrary
            "error": {"id": 19, "markers": {}},
            "style": {"id": 20, "markers": {}},
        }
        self.search_indicators = {"selection": {"id": 21, "positions": []}}
        self.DEBUG_INDICATOR = 22  # Arbitrary
        self.BREAKPOINT_MARKER = 23  # Arbitrary
        self.previous_selection = {
            "line_start": 0,
            "col_start": 0,
            "line_end": 0,
            "col_end": 0,
        }
        if self.path:
            if self.path.endswith(".css"):
                self.lexer = CssLexer()
            elif self.path.endswith(".html") or self.path.endswith(".htm"):
                self.lexer = QsciLexerHTML()
                self.lexer.setDjangoTemplates(True)
            else:
                self.lexer = PythonLexer()
        else:
            self.lexer = PythonLexer()
        self.api = None
        self.has_annotations = False
        self.setModified(False)
        self.breakpoint_handles = set()
        self.configure()

    def contextMenuEvent(self, event):
        """
        A context menu (right click) has been actioned.
        """
        self.context_menu.emit()

    def wheelEvent(self, event):
        """
        Stops QScintilla from doing the wrong sort of zoom handling.
        """
        if not QApplication.keyboardModifiers():
            super().wheelEvent(event)

    def dropEvent(self, event):
        """
        Run by Qt when *something* is dropped on this editor
        """

        # Does the drag event have any urls?
        # Files are transferred as a url (by path not value)
        if event.mimeData().hasUrls():
            # Qt doesn't seem to have an 'open' action,
            # this seems the most appropriate
            event.setDropAction(Qt.DropAction.CopyAction)
            # Valid links
            links = []
            # Iterate over each of the urls attached to the event
            for url in event.mimeData().urls():
                # Check the url is to a local file
                # (not a webpage for example)
                if url.isLocalFile():
                    # Grab a 'real' path from the url
                    path = url.toLocalFile()
                    # Add it to the list of valid links
                    links.append(path)

            # Did we get any?
            if len(links) > 0:
                # Only accept now we actually know we can do
                # something with the drop event
                event.accept()
                for link in links:
                    # Start bubbling an open file request
                    self.open_file.emit(link)

        # If the event wasn't handled let QsciScintilla have a go
        if not event.isAccepted():
            super().dropEvent(event)

    def configure(self):
        """
        Set up the editor component.
        """
        # Font information
        font = Font().load()
        self.setFont(font)
        # Generic editor settings
        self.setUtf8(True)
        self.setAutoIndent(True)
        self.setIndentationsUseTabs(False)
        self.setIndentationWidth(4)
        self.setIndentationGuides(True)
        self.setBackspaceUnindents(True)
        self.setTabWidth(4)
        self.setEdgeColumn(79)
        self.setMarginLineNumbers(0, True)
        self.setMarginWidth(0, 50)
        self.setBraceMatching(QsciScintilla.BraceMatch.SloppyBraceMatch)
        self.SendScintilla(QsciScintilla.SCI_SETHSCROLLBAR, 0)
        self.set_theme()
        # Markers and indicators
        self.setMarginSensitivity(0, True)
        self.markerDefine(QsciScintilla.MarkerSymbol.Circle, self.BREAKPOINT_MARKER)
        self.setMarginSensitivity(1, True)
        # Additional dummy margin to prevent accidental breakpoint toggles when
        # trying to position the edit cursor to the left of the first column,
        # using the mouse and not being 100% accurate. This margin needs to be
        # set with "sensitivity on": otherwise clicking it would select the
        # whole text line, per QsciScintilla's behaviour. It is up to the
        # click handler to ignore clicks on this margin: self.connect_margin.
        self.setMarginWidth(4, 8)
        self.setMarginSensitivity(4, True)
        # Indicators
        self.setIndicatorDrawUnder(True)
        for type_ in self.check_indicators:
            self.indicatorDefine(
                QsciScintilla.IndicatorStyle.SquiggleIndicator,
                self.check_indicators[type_]["id"],
            )
        for type_ in self.search_indicators:
            self.indicatorDefine(
                QsciScintilla.IndicatorStyle.StraightBoxIndicator,
                self.search_indicators[type_]["id"],
            )
        self.indicatorDefine(
            QsciScintilla.IndicatorStyle.FullBoxIndicator, self.DEBUG_INDICATOR
        )
        self.setAnnotationDisplay(QsciScintilla.AnnotationDisplay.AnnotationBoxed)
        self.selectionChanged.connect(self.selection_change_listener)
        self.set_zoom()

    def connect_margin(self, func):
        """
        Connect clicking the margin to the passed in handler function, via a
        filtering handler that ignores clicks on margin 4.
        """

        # Margin 4 motivation in self.configure comments.
        def func_ignoring_margin_4(margin, line, modifiers):
            if margin != 4:
                func(margin, line, modifiers)

        self.marginClicked.connect(func_ignoring_margin_4)

    def set_theme(self, theme=DayTheme):
        """
        Connect the theme to a lexer and return the lexer for the editor to
        apply to the script text.
        """
        theme.apply_to(self.lexer)
        self.lexer.setDefaultPaper(theme.Paper)
        self.setCaretForegroundColor(theme.Caret)
        self.setIndicatorForegroundColor(
            theme.IndicatorError, self.check_indicators["error"]["id"]
        )
        self.setIndicatorForegroundColor(
            theme.IndicatorStyle, self.check_indicators["style"]["id"]
        )
        self.setIndicatorForegroundColor(theme.DebugStyle, self.DEBUG_INDICATOR)
        for type_ in self.search_indicators:
            self.setIndicatorForegroundColor(
                theme.IndicatorWordMatch, self.search_indicators[type_]["id"]
            )
        self.setMarkerBackgroundColor(theme.BreakpointMarker, self.BREAKPOINT_MARKER)
        self.setAutoCompletionThreshold(2)
        self.setAutoCompletionSource(QsciScintilla.AutoCompletionSource.AcsAll)
        self.setLexer(self.lexer)
        self.setMarginsBackgroundColor(theme.Margin)
        self.setMarginsForegroundColor(theme.Caret)
        self.setMatchedBraceBackgroundColor(theme.BraceBackground)
        self.setMatchedBraceForegroundColor(theme.BraceForeground)
        self.setUnmatchedBraceBackgroundColor(theme.UnmatchedBraceBackground)
        self.setUnmatchedBraceForegroundColor(theme.UnmatchedBraceForeground)

    def set_api(self, api_definitions):
        """
        Sets the API entries for tooltips, calltips and the like.
        """
        self.api = QsciAPIs(self.lexer)
        for entry in api_definitions:
            self.api.add(entry)
        self.api.prepare()

    def set_zoom(self, size="m"):
        """
        Sets the font zoom to the specified base point size for all fonts given
        a t-shirt size.
        """
        sizes = {
            "xs": -4,
            "s": -2,
            "m": 1,
            "l": 4,
            "xl": 8,
            "xxl": 16,
            "xxxl": 48,
        }
        self.zoomTo(sizes[size])
        margins = {
            "xs": 30,
            "s": 35,
            "m": 45,
            "l": 50,
            "xl": 60,
            "xxl": 75,
            "xxxl": 85,
        }
        # Make the margin left of line numbers follow zoom level
        self.setMarginWidth(0, margins[size])
        # Make margins around debugger-marker follow zoom level
        self.setMarginWidth(1, int(margins[size] * 0.25))
        self.setMarginWidth(4, int(margins[size] * 0.1))

    @property
    def label(self):
        """
        The label associated with this editor widget (usually the filename of
        the script we're editing).
        """
        if self.path:
            label = os.path.basename(self.path)
        else:
            label = _("untitled")
        return label

    @property
    def title(self):
        """
        The title associated with this editor widget (usually the filename of
        the script we're editing).

        If the script has been modified since it was last saved, the label will
        end with an asterisk.
        """
        if self.isModified():
            return self.label + " •"
        return self.label

    def reset_annotations(self):
        """
        Clears all the assets (indicators, annotations and markers).
        """
        self.clearAnnotations()
        self.markerDeleteAll()
        self.reset_search_indicators()
        self.reset_check_indicators()

    def reset_check_indicators(self):
        """
        Clears all the text indicators related to the check code functionality.
        """
        for indicator in self.check_indicators:
            for _, markers in self.check_indicators[indicator]["markers"].items():
                line_no = markers[0]["line_no"]  # All markers on same line.
                self.clearIndicatorRange(
                    line_no,
                    0,
                    line_no,
                    999999,
                    self.check_indicators[indicator]["id"],
                )
            self.check_indicators[indicator]["markers"] = {}

    def reset_search_indicators(self):
        """
        Clears all the text indicators from the search functionality.
        """
        for indicator in self.search_indicators:
            for position in self.search_indicators[indicator]["positions"]:
                self.clearIndicatorRange(
                    position["line_start"],
                    position["col_start"],
                    position["line_end"],
                    position["col_end"],
                    self.search_indicators[indicator]["id"],
                )
            self.search_indicators[indicator]["positions"] = []

    def annotate_code(self, feedback, annotation_type="error"):
        """
        Given a list of annotations add them to the editor pane so the user can
        act upon them.
        """
        indicator = self.check_indicators[annotation_type]
        for line_no, messages in feedback.items():
            indicator["markers"][line_no] = messages
            for message in messages:
                col = message.get("column", 0)
                if col:
                    col_start = col - 1
                    col_end = col + 1
                    self.fillIndicatorRange(
                        line_no, col_start, line_no, col_end, indicator["id"]
                    )
        if feedback:
            # Ensure the first line with a problem is visible.
            first_problem_line = sorted(feedback.keys())[0]
            self.ensureLineVisible(first_problem_line)

    def debugger_at_line(self, line):
        """
        Set the line to be highlighted with the DEBUG_INDICATOR.
        """
        self.reset_debugger_highlight()
        # Calculate the line length & account for \r\n giving ObOE.
        line_length = len(self.text(line).rstrip())
        self.fillIndicatorRange(line, 0, line, line_length, self.DEBUG_INDICATOR)
        self.ensureLineVisible(line)

    def reset_debugger_highlight(self):
        """
        Reset all the lines so the DEBUG_INDICATOR is no longer displayed.

        We need to check each line since there's no way to tell what the
        currently highlighted line is. This approach also has the advantage of
        resetting the *whole* editor pane.
        """
        for i in range(self.lines()):
            line_length = len(self.text(i))
            self.clearIndicatorRange(i, 0, i, line_length, self.DEBUG_INDICATOR)

    def show_annotations(self):
        """
        Display all the messages to be annotated to the code.
        """
        lines = defaultdict(list)
        for indicator in self.check_indicators:
            markers = self.check_indicators[indicator]["markers"]
            for k, marker_list in markers.items():
                for m in marker_list:
                    lines[m["line_no"]].append("\u2191 " + m["message"])
        for line, messages in lines.items():
            text = "\n".join(messages).strip()
            if text:
                self.annotate(line, text, self.annotationDisplay())

    def find_next_match(
        self,
        text,
        from_line=-1,
        from_col=-1,
        case_sensitive=True,
        wrap_around=True,
    ):
        """
        Finds the next text match from the current cursor, or the given
        position, and selects it (the automatic selection is the only available
        QsciScintilla behaviour).
        Returns True if match found, False otherwise.
        """
        return self.findFirst(
            text,  # Text to find,
            False,  # Treat as regular expression
            case_sensitive,  # Case sensitive search
            True,  # Whole word matches only
            wrap_around,  # Wrap search
            forward=True,  # Forward search
            line=from_line,  # -1 starts at current position
            index=from_col,  # -1 starts at current position
            show=False,  # Unfolds found text
            posix=False,
        )  # More POSIX compatible RegEx

    def range_from_positions(self, start_position, end_position):
        """Given a start-end pair, such as are provided by a regex match,
        return the corresponding Scintilla line-offset pairs which are
        used for searches, indicators etc.

        NOTE: Arguments must be byte offsets into the underlying text bytes.
        """
        start_line, start_offset = self.lineIndexFromPosition(start_position)
        end_line, end_offset = self.lineIndexFromPosition(end_position)
        return start_line, start_offset, end_line, end_offset

    def highlight_selected_matches(self):
        """
        Checks the current selection, if it is a single word it then searches
        and highlights all matches.

        Since we're interested in exactly one word:
        * Ignore an empty selection
        * Ignore anything which spans more than one line
        * Ignore more than one word
        * Ignore anything less than one word
        """
        selected_range = line0, col0, line1, col1 = self.getSelection()
        #
        # If there's no selection, do nothing
        #
        if selected_range == (-1, -1, -1, -1):
            return

        #
        # Ignore anything which spans two or more lines
        #
        if line0 != line1:
            return

        #
        # Ignore if no text is selected or the selected text is not at most one
        # valid identifier-type word.
        #
        selected_text = self.selectedText()
        if not RE_VALID_WORD.match(selected_text):
            return

        #
        # Ignore anything which is not a whole word.
        # NB Although Scintilla defines a SCI_ISRANGEWORD message,
        # it's not exposed by QSciScintilla. Instead, we
        # ask Scintilla for the start end end position of
        # the word we're in and test whether our range end points match
        # those or not.
        #
        pos0 = self.positionFromLineIndex(line0, col0)
        word_start_pos = self.SendScintilla(
            QsciScintilla.SCI_WORDSTARTPOSITION, pos0, 1
        )
        _, start_offset = self.lineIndexFromPosition(word_start_pos)
        if col0 != start_offset:
            return

        pos1 = self.positionFromLineIndex(line1, col1)
        word_end_pos = self.SendScintilla(QsciScintilla.SCI_WORDENDPOSITION, pos1, 1)
        _, end_offset = self.lineIndexFromPosition(word_end_pos)
        if col1 != end_offset:
            return

        #
        # For each matching word within the editor text, add it to
        # the list of highlighted indicators and fill it according
        # to the current theme.
        #
        indicators = self.search_indicators["selection"]
        encoding = "utf8" if self.isUtf8() else "latin1"
        text_bytes = self.text().encode(encoding)
        selected_text_bytes = selected_text.encode(encoding)
        for match in re.finditer(selected_text_bytes, text_bytes):
            range = self.range_from_positions(*match.span())
            #
            # Don't highlight the text we've selected
            #
            if range == selected_range:
                continue

            line_start, col_start, line_end, col_end = range
            indicators["positions"].append(
                {
                    "line_start": line_start,
                    "col_start": col_start,
                    "line_end": line_end,
                    "col_end": col_end,
                }
            )
            self.fillIndicatorRange(
                line_start, col_start, line_end, col_end, indicators["id"]
            )

    def selection_change_listener(self):
        """
        Runs every time the text selection changes. This could get triggered
        multiple times while the mouse click is down, even if selection has not
        changed in itself.
        If there is a new selection is passes control to
        highlight_selected_matches.
        """
        # Get the current selection, exit if it has not changed
        line_from, index_from, line_to, index_to = self.getSelection()
        if (
            self.previous_selection["col_end"] != index_to
            or self.previous_selection["col_start"] != index_from
            or self.previous_selection["line_start"] != line_from
            or self.previous_selection["line_end"] != line_to
        ):
            self.previous_selection["line_start"] = line_from
            self.previous_selection["col_start"] = index_from
            self.previous_selection["line_end"] = line_to
            self.previous_selection["col_end"] = index_to
            # Highlight matches
            self.reset_search_indicators()
            self.highlight_selected_matches()

    def toggle_line(self, raw_line):
        """
        Given a raw_line, will return the toggled version of it.
        """
        clean_line = raw_line.strip()
        if not clean_line or clean_line.startswith("##"):
            # Ignore whitespace-only lines and compact multi-commented lines
            return raw_line

        if clean_line.startswith("#"):
            # It's a comment line, so replace only the first "# " or "#":
            if clean_line.startswith("# "):
                return raw_line.replace("# ", "", 1)
            else:
                return raw_line.replace("#", "", 1)
        else:
            # It's a normal line of code.
            return "# " + raw_line

    def toggle_comments(self):
        """
        Iterate through the selected lines and toggle their comment/uncomment
        state. So, lines that are not comments become comments and vice versa.
        """
        if self.hasSelectedText():
            # Toggle currently selected text.
            logger.info("Toggling comments")
            line_from, index_from, line_to, index_to = self.getSelection()
            selected_text = self.selectedText()
            lines = selected_text.split("\n")
            toggled_lines = []
            for line in lines:
                toggled_lines.append(self.toggle_line(line))
            new_text = "\n".join(toggled_lines)
            self.replaceSelectedText(new_text)
            # Ensure the new text is also selected.
            last_newline = toggled_lines[-1]
            last_oldline = lines[-1]

            # Adjust the selection based on whether the last line got
            # longer, shorter, or stayed the same
            delta = len(last_newline) - len(last_oldline)
            index_to += delta
            self.setSelection(line_from, index_from, line_to, index_to)
        else:
            # Toggle the line currently containing the cursor.
            line_number, column = self.getCursorPosition()
            logger.info("Toggling line {}".format(line_number))
            # Replace CRLF line endings that we add when run on Windows
            line_content = self.text(line_number).replace("\r\n", "\n")
            new_line = self.toggle_line(line_content)
            self.setSelection(line_number, 0, line_number, len(line_content))
            self.replaceSelectedText(new_line)
            self.setSelection(line_number, 0, line_number, len(new_line) - 1)

label property

The label associated with this editor widget (usually the filename of the script we're editing).

title property

The title associated with this editor widget (usually the filename of the script we're editing).

If the script has been modified since it was last saved, the label will end with an asterisk.

annotate_code(feedback, annotation_type='error')

Given a list of annotations add them to the editor pane so the user can act upon them.

Source code in mu/interface/editor.py
def annotate_code(self, feedback, annotation_type="error"):
    """
    Given a list of annotations add them to the editor pane so the user can
    act upon them.
    """
    indicator = self.check_indicators[annotation_type]
    for line_no, messages in feedback.items():
        indicator["markers"][line_no] = messages
        for message in messages:
            col = message.get("column", 0)
            if col:
                col_start = col - 1
                col_end = col + 1
                self.fillIndicatorRange(
                    line_no, col_start, line_no, col_end, indicator["id"]
                )
    if feedback:
        # Ensure the first line with a problem is visible.
        first_problem_line = sorted(feedback.keys())[0]
        self.ensureLineVisible(first_problem_line)

configure()

Set up the editor component.

Source code in mu/interface/editor.py
def configure(self):
    """
    Set up the editor component.
    """
    # Font information
    font = Font().load()
    self.setFont(font)
    # Generic editor settings
    self.setUtf8(True)
    self.setAutoIndent(True)
    self.setIndentationsUseTabs(False)
    self.setIndentationWidth(4)
    self.setIndentationGuides(True)
    self.setBackspaceUnindents(True)
    self.setTabWidth(4)
    self.setEdgeColumn(79)
    self.setMarginLineNumbers(0, True)
    self.setMarginWidth(0, 50)
    self.setBraceMatching(QsciScintilla.BraceMatch.SloppyBraceMatch)
    self.SendScintilla(QsciScintilla.SCI_SETHSCROLLBAR, 0)
    self.set_theme()
    # Markers and indicators
    self.setMarginSensitivity(0, True)
    self.markerDefine(QsciScintilla.MarkerSymbol.Circle, self.BREAKPOINT_MARKER)
    self.setMarginSensitivity(1, True)
    # Additional dummy margin to prevent accidental breakpoint toggles when
    # trying to position the edit cursor to the left of the first column,
    # using the mouse and not being 100% accurate. This margin needs to be
    # set with "sensitivity on": otherwise clicking it would select the
    # whole text line, per QsciScintilla's behaviour. It is up to the
    # click handler to ignore clicks on this margin: self.connect_margin.
    self.setMarginWidth(4, 8)
    self.setMarginSensitivity(4, True)
    # Indicators
    self.setIndicatorDrawUnder(True)
    for type_ in self.check_indicators:
        self.indicatorDefine(
            QsciScintilla.IndicatorStyle.SquiggleIndicator,
            self.check_indicators[type_]["id"],
        )
    for type_ in self.search_indicators:
        self.indicatorDefine(
            QsciScintilla.IndicatorStyle.StraightBoxIndicator,
            self.search_indicators[type_]["id"],
        )
    self.indicatorDefine(
        QsciScintilla.IndicatorStyle.FullBoxIndicator, self.DEBUG_INDICATOR
    )
    self.setAnnotationDisplay(QsciScintilla.AnnotationDisplay.AnnotationBoxed)
    self.selectionChanged.connect(self.selection_change_listener)
    self.set_zoom()

connect_margin(func)

Connect clicking the margin to the passed in handler function, via a filtering handler that ignores clicks on margin 4.

Source code in mu/interface/editor.py
def connect_margin(self, func):
    """
    Connect clicking the margin to the passed in handler function, via a
    filtering handler that ignores clicks on margin 4.
    """

    # Margin 4 motivation in self.configure comments.
    def func_ignoring_margin_4(margin, line, modifiers):
        if margin != 4:
            func(margin, line, modifiers)

    self.marginClicked.connect(func_ignoring_margin_4)

contextMenuEvent(event)

A context menu (right click) has been actioned.

Source code in mu/interface/editor.py
def contextMenuEvent(self, event):
    """
    A context menu (right click) has been actioned.
    """
    self.context_menu.emit()

debugger_at_line(line)

Set the line to be highlighted with the DEBUG_INDICATOR.

Source code in mu/interface/editor.py
def debugger_at_line(self, line):
    """
    Set the line to be highlighted with the DEBUG_INDICATOR.
    """
    self.reset_debugger_highlight()
    # Calculate the line length & account for \r\n giving ObOE.
    line_length = len(self.text(line).rstrip())
    self.fillIndicatorRange(line, 0, line, line_length, self.DEBUG_INDICATOR)
    self.ensureLineVisible(line)

dropEvent(event)

Run by Qt when something is dropped on this editor

Source code in mu/interface/editor.py
def dropEvent(self, event):
    """
    Run by Qt when *something* is dropped on this editor
    """

    # Does the drag event have any urls?
    # Files are transferred as a url (by path not value)
    if event.mimeData().hasUrls():
        # Qt doesn't seem to have an 'open' action,
        # this seems the most appropriate
        event.setDropAction(Qt.DropAction.CopyAction)
        # Valid links
        links = []
        # Iterate over each of the urls attached to the event
        for url in event.mimeData().urls():
            # Check the url is to a local file
            # (not a webpage for example)
            if url.isLocalFile():
                # Grab a 'real' path from the url
                path = url.toLocalFile()
                # Add it to the list of valid links
                links.append(path)

        # Did we get any?
        if len(links) > 0:
            # Only accept now we actually know we can do
            # something with the drop event
            event.accept()
            for link in links:
                # Start bubbling an open file request
                self.open_file.emit(link)

    # If the event wasn't handled let QsciScintilla have a go
    if not event.isAccepted():
        super().dropEvent(event)

find_next_match(text, from_line=-1, from_col=-1, case_sensitive=True, wrap_around=True)

Finds the next text match from the current cursor, or the given position, and selects it (the automatic selection is the only available QsciScintilla behaviour). Returns True if match found, False otherwise.

Source code in mu/interface/editor.py
def find_next_match(
    self,
    text,
    from_line=-1,
    from_col=-1,
    case_sensitive=True,
    wrap_around=True,
):
    """
    Finds the next text match from the current cursor, or the given
    position, and selects it (the automatic selection is the only available
    QsciScintilla behaviour).
    Returns True if match found, False otherwise.
    """
    return self.findFirst(
        text,  # Text to find,
        False,  # Treat as regular expression
        case_sensitive,  # Case sensitive search
        True,  # Whole word matches only
        wrap_around,  # Wrap search
        forward=True,  # Forward search
        line=from_line,  # -1 starts at current position
        index=from_col,  # -1 starts at current position
        show=False,  # Unfolds found text
        posix=False,
    )  # More POSIX compatible RegEx

highlight_selected_matches()

Checks the current selection, if it is a single word it then searches and highlights all matches.

Since we're interested in exactly one word: * Ignore an empty selection * Ignore anything which spans more than one line * Ignore more than one word * Ignore anything less than one word

Source code in mu/interface/editor.py
def highlight_selected_matches(self):
    """
    Checks the current selection, if it is a single word it then searches
    and highlights all matches.

    Since we're interested in exactly one word:
    * Ignore an empty selection
    * Ignore anything which spans more than one line
    * Ignore more than one word
    * Ignore anything less than one word
    """
    selected_range = line0, col0, line1, col1 = self.getSelection()
    #
    # If there's no selection, do nothing
    #
    if selected_range == (-1, -1, -1, -1):
        return

    #
    # Ignore anything which spans two or more lines
    #
    if line0 != line1:
        return

    #
    # Ignore if no text is selected or the selected text is not at most one
    # valid identifier-type word.
    #
    selected_text = self.selectedText()
    if not RE_VALID_WORD.match(selected_text):
        return

    #
    # Ignore anything which is not a whole word.
    # NB Although Scintilla defines a SCI_ISRANGEWORD message,
    # it's not exposed by QSciScintilla. Instead, we
    # ask Scintilla for the start end end position of
    # the word we're in and test whether our range end points match
    # those or not.
    #
    pos0 = self.positionFromLineIndex(line0, col0)
    word_start_pos = self.SendScintilla(
        QsciScintilla.SCI_WORDSTARTPOSITION, pos0, 1
    )
    _, start_offset = self.lineIndexFromPosition(word_start_pos)
    if col0 != start_offset:
        return

    pos1 = self.positionFromLineIndex(line1, col1)
    word_end_pos = self.SendScintilla(QsciScintilla.SCI_WORDENDPOSITION, pos1, 1)
    _, end_offset = self.lineIndexFromPosition(word_end_pos)
    if col1 != end_offset:
        return

    #
    # For each matching word within the editor text, add it to
    # the list of highlighted indicators and fill it according
    # to the current theme.
    #
    indicators = self.search_indicators["selection"]
    encoding = "utf8" if self.isUtf8() else "latin1"
    text_bytes = self.text().encode(encoding)
    selected_text_bytes = selected_text.encode(encoding)
    for match in re.finditer(selected_text_bytes, text_bytes):
        range = self.range_from_positions(*match.span())
        #
        # Don't highlight the text we've selected
        #
        if range == selected_range:
            continue

        line_start, col_start, line_end, col_end = range
        indicators["positions"].append(
            {
                "line_start": line_start,
                "col_start": col_start,
                "line_end": line_end,
                "col_end": col_end,
            }
        )
        self.fillIndicatorRange(
            line_start, col_start, line_end, col_end, indicators["id"]
        )

range_from_positions(start_position, end_position)

Given a start-end pair, such as are provided by a regex match, return the corresponding Scintilla line-offset pairs which are used for searches, indicators etc.

NOTE: Arguments must be byte offsets into the underlying text bytes.

Source code in mu/interface/editor.py
def range_from_positions(self, start_position, end_position):
    """Given a start-end pair, such as are provided by a regex match,
    return the corresponding Scintilla line-offset pairs which are
    used for searches, indicators etc.

    NOTE: Arguments must be byte offsets into the underlying text bytes.
    """
    start_line, start_offset = self.lineIndexFromPosition(start_position)
    end_line, end_offset = self.lineIndexFromPosition(end_position)
    return start_line, start_offset, end_line, end_offset

reset_annotations()

Clears all the assets (indicators, annotations and markers).

Source code in mu/interface/editor.py
def reset_annotations(self):
    """
    Clears all the assets (indicators, annotations and markers).
    """
    self.clearAnnotations()
    self.markerDeleteAll()
    self.reset_search_indicators()
    self.reset_check_indicators()

reset_check_indicators()

Clears all the text indicators related to the check code functionality.

Source code in mu/interface/editor.py
def reset_check_indicators(self):
    """
    Clears all the text indicators related to the check code functionality.
    """
    for indicator in self.check_indicators:
        for _, markers in self.check_indicators[indicator]["markers"].items():
            line_no = markers[0]["line_no"]  # All markers on same line.
            self.clearIndicatorRange(
                line_no,
                0,
                line_no,
                999999,
                self.check_indicators[indicator]["id"],
            )
        self.check_indicators[indicator]["markers"] = {}

reset_debugger_highlight()

Reset all the lines so the DEBUG_INDICATOR is no longer displayed.

We need to check each line since there's no way to tell what the currently highlighted line is. This approach also has the advantage of resetting the whole editor pane.

Source code in mu/interface/editor.py
def reset_debugger_highlight(self):
    """
    Reset all the lines so the DEBUG_INDICATOR is no longer displayed.

    We need to check each line since there's no way to tell what the
    currently highlighted line is. This approach also has the advantage of
    resetting the *whole* editor pane.
    """
    for i in range(self.lines()):
        line_length = len(self.text(i))
        self.clearIndicatorRange(i, 0, i, line_length, self.DEBUG_INDICATOR)

reset_search_indicators()

Clears all the text indicators from the search functionality.

Source code in mu/interface/editor.py
def reset_search_indicators(self):
    """
    Clears all the text indicators from the search functionality.
    """
    for indicator in self.search_indicators:
        for position in self.search_indicators[indicator]["positions"]:
            self.clearIndicatorRange(
                position["line_start"],
                position["col_start"],
                position["line_end"],
                position["col_end"],
                self.search_indicators[indicator]["id"],
            )
        self.search_indicators[indicator]["positions"] = []

selection_change_listener()

Runs every time the text selection changes. This could get triggered multiple times while the mouse click is down, even if selection has not changed in itself. If there is a new selection is passes control to highlight_selected_matches.

Source code in mu/interface/editor.py
def selection_change_listener(self):
    """
    Runs every time the text selection changes. This could get triggered
    multiple times while the mouse click is down, even if selection has not
    changed in itself.
    If there is a new selection is passes control to
    highlight_selected_matches.
    """
    # Get the current selection, exit if it has not changed
    line_from, index_from, line_to, index_to = self.getSelection()
    if (
        self.previous_selection["col_end"] != index_to
        or self.previous_selection["col_start"] != index_from
        or self.previous_selection["line_start"] != line_from
        or self.previous_selection["line_end"] != line_to
    ):
        self.previous_selection["line_start"] = line_from
        self.previous_selection["col_start"] = index_from
        self.previous_selection["line_end"] = line_to
        self.previous_selection["col_end"] = index_to
        # Highlight matches
        self.reset_search_indicators()
        self.highlight_selected_matches()

set_api(api_definitions)

Sets the API entries for tooltips, calltips and the like.

Source code in mu/interface/editor.py
def set_api(self, api_definitions):
    """
    Sets the API entries for tooltips, calltips and the like.
    """
    self.api = QsciAPIs(self.lexer)
    for entry in api_definitions:
        self.api.add(entry)
    self.api.prepare()

set_theme(theme=DayTheme)

Connect the theme to a lexer and return the lexer for the editor to apply to the script text.

Source code in mu/interface/editor.py
def set_theme(self, theme=DayTheme):
    """
    Connect the theme to a lexer and return the lexer for the editor to
    apply to the script text.
    """
    theme.apply_to(self.lexer)
    self.lexer.setDefaultPaper(theme.Paper)
    self.setCaretForegroundColor(theme.Caret)
    self.setIndicatorForegroundColor(
        theme.IndicatorError, self.check_indicators["error"]["id"]
    )
    self.setIndicatorForegroundColor(
        theme.IndicatorStyle, self.check_indicators["style"]["id"]
    )
    self.setIndicatorForegroundColor(theme.DebugStyle, self.DEBUG_INDICATOR)
    for type_ in self.search_indicators:
        self.setIndicatorForegroundColor(
            theme.IndicatorWordMatch, self.search_indicators[type_]["id"]
        )
    self.setMarkerBackgroundColor(theme.BreakpointMarker, self.BREAKPOINT_MARKER)
    self.setAutoCompletionThreshold(2)
    self.setAutoCompletionSource(QsciScintilla.AutoCompletionSource.AcsAll)
    self.setLexer(self.lexer)
    self.setMarginsBackgroundColor(theme.Margin)
    self.setMarginsForegroundColor(theme.Caret)
    self.setMatchedBraceBackgroundColor(theme.BraceBackground)
    self.setMatchedBraceForegroundColor(theme.BraceForeground)
    self.setUnmatchedBraceBackgroundColor(theme.UnmatchedBraceBackground)
    self.setUnmatchedBraceForegroundColor(theme.UnmatchedBraceForeground)

set_zoom(size='m')

Sets the font zoom to the specified base point size for all fonts given a t-shirt size.

Source code in mu/interface/editor.py
def set_zoom(self, size="m"):
    """
    Sets the font zoom to the specified base point size for all fonts given
    a t-shirt size.
    """
    sizes = {
        "xs": -4,
        "s": -2,
        "m": 1,
        "l": 4,
        "xl": 8,
        "xxl": 16,
        "xxxl": 48,
    }
    self.zoomTo(sizes[size])
    margins = {
        "xs": 30,
        "s": 35,
        "m": 45,
        "l": 50,
        "xl": 60,
        "xxl": 75,
        "xxxl": 85,
    }
    # Make the margin left of line numbers follow zoom level
    self.setMarginWidth(0, margins[size])
    # Make margins around debugger-marker follow zoom level
    self.setMarginWidth(1, int(margins[size] * 0.25))
    self.setMarginWidth(4, int(margins[size] * 0.1))

show_annotations()

Display all the messages to be annotated to the code.

Source code in mu/interface/editor.py
def show_annotations(self):
    """
    Display all the messages to be annotated to the code.
    """
    lines = defaultdict(list)
    for indicator in self.check_indicators:
        markers = self.check_indicators[indicator]["markers"]
        for k, marker_list in markers.items():
            for m in marker_list:
                lines[m["line_no"]].append("\u2191 " + m["message"])
    for line, messages in lines.items():
        text = "\n".join(messages).strip()
        if text:
            self.annotate(line, text, self.annotationDisplay())

toggle_comments()

Iterate through the selected lines and toggle their comment/uncomment state. So, lines that are not comments become comments and vice versa.

Source code in mu/interface/editor.py
def toggle_comments(self):
    """
    Iterate through the selected lines and toggle their comment/uncomment
    state. So, lines that are not comments become comments and vice versa.
    """
    if self.hasSelectedText():
        # Toggle currently selected text.
        logger.info("Toggling comments")
        line_from, index_from, line_to, index_to = self.getSelection()
        selected_text = self.selectedText()
        lines = selected_text.split("\n")
        toggled_lines = []
        for line in lines:
            toggled_lines.append(self.toggle_line(line))
        new_text = "\n".join(toggled_lines)
        self.replaceSelectedText(new_text)
        # Ensure the new text is also selected.
        last_newline = toggled_lines[-1]
        last_oldline = lines[-1]

        # Adjust the selection based on whether the last line got
        # longer, shorter, or stayed the same
        delta = len(last_newline) - len(last_oldline)
        index_to += delta
        self.setSelection(line_from, index_from, line_to, index_to)
    else:
        # Toggle the line currently containing the cursor.
        line_number, column = self.getCursorPosition()
        logger.info("Toggling line {}".format(line_number))
        # Replace CRLF line endings that we add when run on Windows
        line_content = self.text(line_number).replace("\r\n", "\n")
        new_line = self.toggle_line(line_content)
        self.setSelection(line_number, 0, line_number, len(line_content))
        self.replaceSelectedText(new_line)
        self.setSelection(line_number, 0, line_number, len(new_line) - 1)

toggle_line(raw_line)

Given a raw_line, will return the toggled version of it.

Source code in mu/interface/editor.py
def toggle_line(self, raw_line):
    """
    Given a raw_line, will return the toggled version of it.
    """
    clean_line = raw_line.strip()
    if not clean_line or clean_line.startswith("##"):
        # Ignore whitespace-only lines and compact multi-commented lines
        return raw_line

    if clean_line.startswith("#"):
        # It's a comment line, so replace only the first "# " or "#":
        if clean_line.startswith("# "):
            return raw_line.replace("# ", "", 1)
        else:
            return raw_line.replace("#", "", 1)
    else:
        # It's a normal line of code.
        return "# " + raw_line

wheelEvent(event)

Stops QScintilla from doing the wrong sort of zoom handling.

Source code in mu/interface/editor.py
def wheelEvent(self, event):
    """
    Stops QScintilla from doing the wrong sort of zoom handling.
    """
    if not QApplication.keyboardModifiers():
        super().wheelEvent(event)

PythonLexer

Bases: QsciLexerPython

A Python specific "lexer" that's used to identify keywords of the Python language so the editor can do syntax highlighting.

Source code in mu/interface/editor.py
class PythonLexer(QsciLexerPython):
    """
    A Python specific "lexer" that's used to identify keywords of the Python
    language so the editor can do syntax highlighting.
    """

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setHighlightSubidentifiers(False)

    def keywords(self, flag):
        """
        Returns a list of Python keywords.
        """
        if flag == 1:
            kws = keyword.kwlist + ["self", "cls"]
        elif flag == 2:
            kws = __builtins__.keys()
        else:
            return None
        return " ".join(kws)

keywords(flag)

Returns a list of Python keywords.

Source code in mu/interface/editor.py
def keywords(self, flag):
    """
    Returns a list of Python keywords.
    """
    if flag == 1:
        kws = keyword.kwlist + ["self", "cls"]
    elif flag == 2:
        kws = __builtins__.keys()
    else:
        return None
    return " ".join(kws)

mu.interface.panes

Contains code used to populate the various panes found in the user interface (REPL, file list, debug inspector etc...).

mu.interface.panes

Contains the UI classes used to populate the various panes used by Mu.

Copyright (c) 2015-2017 Nicholas H.Tollervey and others (see the AUTHORS file).

This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/.

DebugInspector

Bases: QTreeView

Presents a tree like representation of the current state of the call stack to the user.

Source code in mu/interface/panes.py
class DebugInspector(QTreeView):
    """
    Presents a tree like representation of the current state of the call stack
    to the user.
    """

    def __init__(self):
        super().__init__()
        self.setUniformRowHeights(True)
        self.setSelectionBehavior(QTreeView.SelectionBehavior.SelectRows)
        # Record row expansion/collapse to keep dicts expanded on update
        self.expanded.connect(self.record_expanded)
        self.collapsed.connect(self.record_collapsed)
        self.expanded_dicts = set()

    def record_expanded(self, index):
        """
        Keep track of expanded dicts for displaying in debugger.
        """
        expanded = self.model().itemFromIndex(index).text()
        self.expanded_dicts.add(expanded)

    def record_collapsed(self, index):
        """
        Remove collapsed dicts from set, so they render collapsed.
        """
        collapsed = self.model().itemFromIndex(index).text()
        if collapsed in self.expanded_dicts:
            self.expanded_dicts.remove(collapsed)

    def set_font_size(self, new_size=DEFAULT_FONT_SIZE):
        """
        Sets the font size for all the textual elements in this pane.
        """
        stylesheet = (
            "QWidget{font-size: " + str(new_size) + "pt; font-family: Monospace;}"
        )
        self.setStyleSheet(stylesheet)

    def set_zoom(self, size):
        """
        Set the current zoom level given the "t-shirt" size.
        """
        self.set_font_size(PANE_ZOOM_SIZES[size])

    def set_theme(self, theme):
        pass

record_collapsed(index)

Remove collapsed dicts from set, so they render collapsed.

Source code in mu/interface/panes.py
def record_collapsed(self, index):
    """
    Remove collapsed dicts from set, so they render collapsed.
    """
    collapsed = self.model().itemFromIndex(index).text()
    if collapsed in self.expanded_dicts:
        self.expanded_dicts.remove(collapsed)

record_expanded(index)

Keep track of expanded dicts for displaying in debugger.

Source code in mu/interface/panes.py
def record_expanded(self, index):
    """
    Keep track of expanded dicts for displaying in debugger.
    """
    expanded = self.model().itemFromIndex(index).text()
    self.expanded_dicts.add(expanded)

set_font_size(new_size=DEFAULT_FONT_SIZE)

Sets the font size for all the textual elements in this pane.

Source code in mu/interface/panes.py
def set_font_size(self, new_size=DEFAULT_FONT_SIZE):
    """
    Sets the font size for all the textual elements in this pane.
    """
    stylesheet = (
        "QWidget{font-size: " + str(new_size) + "pt; font-family: Monospace;}"
    )
    self.setStyleSheet(stylesheet)

set_zoom(size)

Set the current zoom level given the "t-shirt" size.

Source code in mu/interface/panes.py
def set_zoom(self, size):
    """
    Set the current zoom level given the "t-shirt" size.
    """
    self.set_font_size(PANE_ZOOM_SIZES[size])

JupyterREPLPane

Bases: RichJupyterWidget

REPL = Read, Evaluate, Print, Loop.

Displays a Jupyter iPython session.

Source code in mu/interface/panes.py
class JupyterREPLPane(RichJupyterWidget):
    """
    REPL = Read, Evaluate, Print, Loop.

    Displays a Jupyter iPython session.
    """

    on_append_text = pyqtSignal(bytes)

    def __init__(self, theme="day", parent=None):
        super().__init__(parent)
        self.set_theme(theme)
        self.console_height = 10

    def _append_plain_text(self, text, *args, **kwargs):
        """
        Ensures appended text is emitted as a signal with associated bytes.
        """
        super()._append_plain_text(text, *args, **kwargs)
        self.on_append_text.emit(text.encode("utf-8"))

    def set_font_size(self, new_size=DEFAULT_FONT_SIZE):
        """
        Sets the font size for all the textual elements in this pane.
        """
        font = self.font
        font.setPointSize(new_size)
        self._set_font(font)

    def set_zoom(self, size):
        """
        Set the current zoom level given the "t-shirt" size.
        """
        self.set_font_size(PANE_ZOOM_SIZES[size])

    def set_theme(self, theme):
        """
        Sets the theme / look for the REPL pane.
        """
        if theme == "contrast":
            self.style_sheet = CONTRAST_STYLE
            self.syntax_style = "bw"
        elif theme == "night":
            self.style_sheet = NIGHT_STYLE
            self.syntax_style = "monokai"
        else:
            self.style_sheet = DAY_STYLE
            self.syntax_style = "default"

    def setFocus(self):
        """
        Override base setFocus so the focus happens to the embedded _control
        within this widget.
        """
        self._control.setFocus()

setFocus()

Override base setFocus so the focus happens to the embedded _control within this widget.

Source code in mu/interface/panes.py
def setFocus(self):
    """
    Override base setFocus so the focus happens to the embedded _control
    within this widget.
    """
    self._control.setFocus()

set_font_size(new_size=DEFAULT_FONT_SIZE)

Sets the font size for all the textual elements in this pane.

Source code in mu/interface/panes.py
def set_font_size(self, new_size=DEFAULT_FONT_SIZE):
    """
    Sets the font size for all the textual elements in this pane.
    """
    font = self.font
    font.setPointSize(new_size)
    self._set_font(font)

set_theme(theme)

Sets the theme / look for the REPL pane.

Source code in mu/interface/panes.py
def set_theme(self, theme):
    """
    Sets the theme / look for the REPL pane.
    """
    if theme == "contrast":
        self.style_sheet = CONTRAST_STYLE
        self.syntax_style = "bw"
    elif theme == "night":
        self.style_sheet = NIGHT_STYLE
        self.syntax_style = "monokai"
    else:
        self.style_sheet = DAY_STYLE
        self.syntax_style = "default"

set_zoom(size)

Set the current zoom level given the "t-shirt" size.

Source code in mu/interface/panes.py
def set_zoom(self, size):
    """
    Set the current zoom level given the "t-shirt" size.
    """
    self.set_font_size(PANE_ZOOM_SIZES[size])

MicroPythonREPLPane

Bases: QTextEdit

REPL = Read, Evaluate, Print, Loop.

This widget represents a REPL client connected to a device running MicroPython.

The device MUST be flashed with MicroPython for this to work.

Source code in mu/interface/panes.py
class MicroPythonREPLPane(QTextEdit):
    """
    REPL = Read, Evaluate, Print, Loop.

    This widget represents a REPL client connected to a device running
    MicroPython.

    The device MUST be flashed with MicroPython for this to work.
    """

    def __init__(self, connection, theme="day", parent=None):
        super().__init__(parent)
        self.connection = connection
        self.setFont(Font().load())
        self.font_size = DEFAULT_FONT_SIZE
        self.setAcceptRichText(False)
        self.setReadOnly(False)
        self.setUndoRedoEnabled(False)
        self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
        self.customContextMenuRequested.connect(self.context_menu)
        # The following variable maintains the position where we know
        # the device cursor is placed. It is initialized to the beginning
        # of the QTextEdit (i.e. equal to the Qt cursor position)
        self.device_cursor_position = self.textCursor().position()
        self.setObjectName("replpane")
        self.set_theme(theme)
        self.unprocessed_input = b""  # used by process_bytes
        self.decoder = codecs.getincrementaldecoder("utf8")("replace")
        # CSI sequence: ESC [ <params> <final letter>, where params are
        # semicolon-separated numbers. Each repeated parameter is anchored on a
        # literal ';' (non-capturing) so the digits can't overlap with `count`;
        # the old `(;?[\d]*)*` form was ambiguous and caused exponential
        # backtracking on inputs like "\x1B[" + "0" * n with no final letter.
        self.vt100_regex = re.compile(
            r"\x1B\[(?P<count>\d*)(?:;\d*)*(?P<action>[A-Za-z])"
        )
        self.osc_regex = re.compile(
            r"\x1B\](?P<command>[\d]*);(?P<string>[^\x1B]*)\x1B\\"
        )

    def insertFromMimeData(self, source):
        """
        Insert mime data by sending it to the REPL
        """
        if source and source.text():
            to_paste = source.text().replace("\n", "\r").replace("\r\r", "\r")
            if "\r" in to_paste:
                # Enter MicroPython's paste mode for multi-line pastes so
                # indentation isn't messed up.
                self.connection.write(b"\x05")  # Enter paste mode.
                self.connection.write(bytes(to_paste, "utf8"))  # Paste.
                self.connection.write(b"\x04")  # Exit paste mode.
            else:
                # Only a fragment to paste, so just insert as is.
                self.connection.write(bytes(to_paste, "utf8"))  # Paste.

    def context_menu(self):
        """
        Creates custom context menu with just copy and paste.
        """
        menu = QMenu(self)
        if platform.system() == "Darwin":
            copy_keys = QKeySequence(Qt.Modifier.CTRL | Qt.Key.Key_C)
            paste_keys = QKeySequence(Qt.Modifier.CTRL | Qt.Key.Key_V)
        else:
            copy_keys = QKeySequence(
                Qt.Modifier.CTRL | Qt.Modifier.SHIFT | Qt.Key.Key_C
            )
            paste_keys = QKeySequence(
                Qt.Modifier.CTRL | Qt.Modifier.SHIFT | Qt.Key.Key_V
            )

        menu.addAction("Copy", self.copy, copy_keys)
        menu.addAction("Paste", self.paste, paste_keys)
        menu.exec(QCursor.pos())

    def set_theme(self, theme):
        self.set_font_size(self.font_size)

    def send(self, msg):
        self.connection.write(msg)

    def keyPressEvent(self, data):
        """
        Called when the user types something in the REPL.

        Correctly encodes it and sends it to the connected device.
        """
        tc = self.textCursor()
        key = data.key()
        ctrl_only = data.modifiers() == Qt.KeyboardModifier.ControlModifier
        meta_only = data.modifiers() == Qt.KeyboardModifier.MetaModifier
        ctrl_shift_only = (
            data.modifiers()
            == Qt.KeyboardModifier.ControlModifier | Qt.KeyboardModifier.ShiftModifier
        )
        shift_down = data.modifiers() & Qt.KeyboardModifier.ShiftModifier
        on_osx = platform.system() == "Darwin"

        if key == Qt.Key.Key_Return:
            # Move cursor to the end of document before sending carriage return
            tc.movePosition(
                QTextCursor.MoveOperation.End,
                mode=QTextCursor.MoveMode.MoveAnchor,
            )
            self.device_cursor_position = tc.position()
            self.send(VT100_RETURN)
        elif key == Qt.Key.Key_Backspace:
            if not self.delete_selection():
                self.send(VT100_BACKSPACE)
        elif key == Qt.Key.Key_Delete:
            if not self.delete_selection():
                self.send(VT100_DELETE)
        elif key == Qt.Key.Key_Up:
            self.send(VT100_UP)
        elif key == Qt.Key.Key_Down:
            self.send(VT100_DOWN)
        elif key == Qt.Key.Key_Right:
            if shift_down:
                # Text selection - pass down
                super().keyPressEvent(data)
            elif tc.hasSelection():
                self.move_cursor_to(tc.selectionEnd())
            else:
                self.send(VT100_RIGHT)
        elif key == Qt.Key.Key_Left:
            if shift_down:
                # Text selection - pass down
                super().keyPressEvent(data)
            elif tc.hasSelection():
                self.move_cursor_to(tc.selectionStart())
            else:
                self.send(VT100_LEFT)
        elif key == Qt.Key.Key_Home:
            self.send(VT100_HOME)
        elif key == Qt.Key.Key_End:
            self.send(VT100_END)
        elif (on_osx and meta_only) or (not on_osx and ctrl_only):
            # Handle the Control key. On OSX/macOS/Darwin (python calls this
            # platform Darwin), this is handled by the Meta modifier. Other
            # platforms (Linux, Windows) call this the Control modifier. Go
            # figure. See http://doc.qt.io/qt-5/qt.html#KeyboardModifier-enum
            if Qt.Key.Key_A <= key <= Qt.Key.Key_Z:
                # The microbit treats an input of \x01 as Ctrl+A, etc.
                self.send(bytes([1 + key - Qt.Key.Key_A]))
        elif ctrl_shift_only or (on_osx and ctrl_only):
            # Command-key on Mac, Ctrl-Shift on Win/Lin
            if key == Qt.Key.Key_C:
                self.copy()
            elif key == Qt.Key.Key_V:
                self.delete_selection()
                self.paste()
        else:
            self.delete_selection()
            self.send(bytes(data.text(), "utf8"))

    def set_qtcursor_to_devicecursor(self):
        """
        Resets the Qt TextCursor to where we know the device has the cursor
        placed.
        """
        tc = self.textCursor()
        tc.setPosition(self.device_cursor_position)
        self.setTextCursor(tc)

    def set_devicecursor_to_qtcursor(self):
        """
        Call this whenever the cursor has been moved by the user, to send
        the cursor movement to the device.
        """
        self.move_cursor_to(self.textCursor().position())

    def move_cursor_to(self, new_position):
        """Move the cursor, by sending vt100 left/right signals through
        serial. The Qt cursor is first returned to the known location
        of the device cursor.  Then the appropriate number of move
        left or right signals are send.  The Qt cursor is not moved to
        the new_position here, but will be moved once receiving a
        response (in process_tty_data).
        """
        # Reset Qt cursor position
        self.set_qtcursor_to_devicecursor()
        # Calculate number of steps
        steps = new_position - self.device_cursor_position

        # Limit cursor movement to the last line
        if steps < 0:
            cursor = self.textCursor()
            cursor.setPosition(self.device_cursor_position)
            cursor.movePosition(
                QTextCursor.MoveOperation.StartOfLine,
                mode=QTextCursor.MoveMode.KeepAnchor,
            )
            line_start = cursor.selectionStart()
            if line_start > new_position:
                return

        # Send the appropriate right/left moves
        if steps > 0:
            # Move cursor right if positive
            self.send(VT100_RIGHT * steps)
        elif steps < 0:
            # Move cursor left if negative
            self.send(VT100_LEFT * abs(steps))

    def delete_selection(self):
        """
        Returns true if deletion happened, returns false if there was no
        selection to delete.
        """
        tc = self.textCursor()
        if tc.hasSelection():
            # Calculate how much should be deleted (N)
            selectionSize = tc.selectionEnd() - tc.selectionStart()
            # Move cursor to end of selection
            self.move_cursor_to(tc.selectionEnd())
            # Send N backspaces
            self.send(VT100_BACKSPACE * selectionSize)
            return True
        return False

    def mouseReleaseEvent(self, mouseEvent):
        """Called whenever a user have had a mouse button pressed, and
        releases it. We pass it through to the normal way Qt handles
        button pressed, but also sends as cursor movement signal to
        the device (except if a selection is made, for selections we first
        move the cursor on deselection)
        """
        super().mouseReleaseEvent(mouseEvent)

        # If when a user have clicked and not made a selection
        # move the device cursor to where the user clicked
        if not self.textCursor().hasSelection():
            self.set_devicecursor_to_qtcursor()

    def process_tty_data(self, data):
        """
        Given some incoming bytes of data, work out how to handle / display
        them in the REPL widget.
        If received input is incomplete, stores remainder in
        self.unprocessed_input.

        Updates the self.device_cursor_position to match that of the device
        for every input received.
        """
        i = 0
        data = self.decoder.decode(data)
        if len(self.unprocessed_input) > 0:
            # Prepend bytes from last time, that wasn't processed
            data = self.unprocessed_input + data
            self.unprocessed_input = ""

        # Reset cursor. E.g. if doing a selection, the qt cursor and
        # device cursor will not match, we reset it here to make sure
        # they do match (this removes any selections when new input is
        # received)
        self.set_qtcursor_to_devicecursor()
        tc = self.textCursor()

        while i < len(data):
            if data[i] == "\b":
                tc.movePosition(QTextCursor.MoveOperation.Left)
                self.device_cursor_position = tc.position()
            elif data[i] == "\r":
                # Carriage return. Do nothing, we handle newlines when
                # reading \n
                pass
            elif data[i] == "\x1b":
                # Escape
                if len(data) > i + 1 and data[i + 1] == "[":
                    # VT100 cursor detected: <Esc>[
                    match = self.vt100_regex.search(data[i:])
                    if match:
                        # move to (almost) after control seq
                        # (will ++ at end of loop)
                        i += match.end() - 1
                        count_string = match.group("count")
                        count = 1 if count_string == "" else int(count_string)
                        action = match.group("action")
                        if action == "A":  # up
                            tc.movePosition(QTextCursor.MoveOperation.Up, n=count)
                            self.device_cursor_position = tc.position()
                        elif action == "B":  # down
                            tc.movePosition(QTextCursor.MoveOperation.Down, n=count)
                            self.device_cursor_position = tc.position()
                        elif action == "C":  # right
                            tc.movePosition(QTextCursor.MoveOperation.Right, n=count)
                            self.device_cursor_position = tc.position()
                        elif action == "D":  # left
                            tc.movePosition(QTextCursor.MoveOperation.Left, n=count)
                            self.device_cursor_position = tc.position()
                        elif action == "K":  # delete things
                            if count_string == "":  # delete to end of line
                                tc.movePosition(
                                    QTextCursor.MoveOperation.EndOfLine,
                                    mode=QTextCursor.MoveMode.KeepAnchor,
                                )
                                tc.removeSelectedText()
                                self.device_cursor_position = tc.position()
                        else:
                            # Unknown action, log warning and ignore
                            command = match.group(0).replace("\x1b", "<Esc>")
                            msg = "Received unsupported VT100 command: {}"
                            logger.warning(msg.format(command))
                    else:
                        # Cursor detected, but no match, must be
                        # incomplete input
                        self.unprocessed_input = data[i:]
                        break
                elif len(data) > i + 1 and data[i + 1] == "]":
                    # OSC cursor detected: <Esc>]
                    match = self.osc_regex.search(data[i:])
                    if match:
                        # move to (almost) after control seq
                        # (will ++ at end of loop)
                        i += match.end() - 1
                        command = match.group("command")
                        string = match.group("string")
                        if command == "0":  # Set window title and icon name
                            logger.warning("dropped title {}".format(string))
                        else:
                            # Unknown action, log warning and ignore
                            command = match.group(0).replace("\x1b", "<Esc>")
                            msg = "Received unsupported VT100 command: {}"
                            logger.warning(msg.format(command))
                    else:
                        # Cursor detected, but no match, must be
                        # incomplete input
                        self.unprocessed_input = data[i:]
                        break
                elif len(data) == i + 1:
                    # Escape received as end of transmission. Perhaps
                    # the transmission is incomplete, wait until next
                    # bytes are received to determine what to do
                    self.unprocessed_input = data[i:]
                    break
            elif data[i] == "\n":
                tc.movePosition(QTextCursor.MoveOperation.End)
                self.device_cursor_position = tc.position() + 1
                self.setTextCursor(tc)
                self.insertPlainText(data[i])
            else:
                # Char received, with VT100 that should be interpreted
                # as overwrite the char in front of the cursor
                tc.deleteChar()
                self.device_cursor_position = tc.position() + 1
                self.insertPlainText(data[i])
            self.setTextCursor(tc)
            i += 1
        # Scroll textarea if necessary to see cursor
        self.ensureCursorVisible()

    def clear(self):
        """
        Clears the text of the REPL.
        """
        self.setText("")

    def set_font_size(self, new_size=DEFAULT_FONT_SIZE):
        """
        Sets the font size for all the textual elements in this pane.
        """
        self.font_size = new_size
        font = self.font()
        font.setPointSize(new_size)
        self.setFont(font)

    def set_zoom(self, size):
        """
        Set the current zoom level given the "t-shirt" size.
        """
        self.set_font_size(PANE_ZOOM_SIZES[size])

clear()

Clears the text of the REPL.

Source code in mu/interface/panes.py
def clear(self):
    """
    Clears the text of the REPL.
    """
    self.setText("")

context_menu()

Creates custom context menu with just copy and paste.

Source code in mu/interface/panes.py
def context_menu(self):
    """
    Creates custom context menu with just copy and paste.
    """
    menu = QMenu(self)
    if platform.system() == "Darwin":
        copy_keys = QKeySequence(Qt.Modifier.CTRL | Qt.Key.Key_C)
        paste_keys = QKeySequence(Qt.Modifier.CTRL | Qt.Key.Key_V)
    else:
        copy_keys = QKeySequence(
            Qt.Modifier.CTRL | Qt.Modifier.SHIFT | Qt.Key.Key_C
        )
        paste_keys = QKeySequence(
            Qt.Modifier.CTRL | Qt.Modifier.SHIFT | Qt.Key.Key_V
        )

    menu.addAction("Copy", self.copy, copy_keys)
    menu.addAction("Paste", self.paste, paste_keys)
    menu.exec(QCursor.pos())

delete_selection()

Returns true if deletion happened, returns false if there was no selection to delete.

Source code in mu/interface/panes.py
def delete_selection(self):
    """
    Returns true if deletion happened, returns false if there was no
    selection to delete.
    """
    tc = self.textCursor()
    if tc.hasSelection():
        # Calculate how much should be deleted (N)
        selectionSize = tc.selectionEnd() - tc.selectionStart()
        # Move cursor to end of selection
        self.move_cursor_to(tc.selectionEnd())
        # Send N backspaces
        self.send(VT100_BACKSPACE * selectionSize)
        return True
    return False

insertFromMimeData(source)

Insert mime data by sending it to the REPL

Source code in mu/interface/panes.py
def insertFromMimeData(self, source):
    """
    Insert mime data by sending it to the REPL
    """
    if source and source.text():
        to_paste = source.text().replace("\n", "\r").replace("\r\r", "\r")
        if "\r" in to_paste:
            # Enter MicroPython's paste mode for multi-line pastes so
            # indentation isn't messed up.
            self.connection.write(b"\x05")  # Enter paste mode.
            self.connection.write(bytes(to_paste, "utf8"))  # Paste.
            self.connection.write(b"\x04")  # Exit paste mode.
        else:
            # Only a fragment to paste, so just insert as is.
            self.connection.write(bytes(to_paste, "utf8"))  # Paste.

keyPressEvent(data)

Called when the user types something in the REPL.

Correctly encodes it and sends it to the connected device.

Source code in mu/interface/panes.py
def keyPressEvent(self, data):
    """
    Called when the user types something in the REPL.

    Correctly encodes it and sends it to the connected device.
    """
    tc = self.textCursor()
    key = data.key()
    ctrl_only = data.modifiers() == Qt.KeyboardModifier.ControlModifier
    meta_only = data.modifiers() == Qt.KeyboardModifier.MetaModifier
    ctrl_shift_only = (
        data.modifiers()
        == Qt.KeyboardModifier.ControlModifier | Qt.KeyboardModifier.ShiftModifier
    )
    shift_down = data.modifiers() & Qt.KeyboardModifier.ShiftModifier
    on_osx = platform.system() == "Darwin"

    if key == Qt.Key.Key_Return:
        # Move cursor to the end of document before sending carriage return
        tc.movePosition(
            QTextCursor.MoveOperation.End,
            mode=QTextCursor.MoveMode.MoveAnchor,
        )
        self.device_cursor_position = tc.position()
        self.send(VT100_RETURN)
    elif key == Qt.Key.Key_Backspace:
        if not self.delete_selection():
            self.send(VT100_BACKSPACE)
    elif key == Qt.Key.Key_Delete:
        if not self.delete_selection():
            self.send(VT100_DELETE)
    elif key == Qt.Key.Key_Up:
        self.send(VT100_UP)
    elif key == Qt.Key.Key_Down:
        self.send(VT100_DOWN)
    elif key == Qt.Key.Key_Right:
        if shift_down:
            # Text selection - pass down
            super().keyPressEvent(data)
        elif tc.hasSelection():
            self.move_cursor_to(tc.selectionEnd())
        else:
            self.send(VT100_RIGHT)
    elif key == Qt.Key.Key_Left:
        if shift_down:
            # Text selection - pass down
            super().keyPressEvent(data)
        elif tc.hasSelection():
            self.move_cursor_to(tc.selectionStart())
        else:
            self.send(VT100_LEFT)
    elif key == Qt.Key.Key_Home:
        self.send(VT100_HOME)
    elif key == Qt.Key.Key_End:
        self.send(VT100_END)
    elif (on_osx and meta_only) or (not on_osx and ctrl_only):
        # Handle the Control key. On OSX/macOS/Darwin (python calls this
        # platform Darwin), this is handled by the Meta modifier. Other
        # platforms (Linux, Windows) call this the Control modifier. Go
        # figure. See http://doc.qt.io/qt-5/qt.html#KeyboardModifier-enum
        if Qt.Key.Key_A <= key <= Qt.Key.Key_Z:
            # The microbit treats an input of \x01 as Ctrl+A, etc.
            self.send(bytes([1 + key - Qt.Key.Key_A]))
    elif ctrl_shift_only or (on_osx and ctrl_only):
        # Command-key on Mac, Ctrl-Shift on Win/Lin
        if key == Qt.Key.Key_C:
            self.copy()
        elif key == Qt.Key.Key_V:
            self.delete_selection()
            self.paste()
    else:
        self.delete_selection()
        self.send(bytes(data.text(), "utf8"))

mouseReleaseEvent(mouseEvent)

Called whenever a user have had a mouse button pressed, and releases it. We pass it through to the normal way Qt handles button pressed, but also sends as cursor movement signal to the device (except if a selection is made, for selections we first move the cursor on deselection)

Source code in mu/interface/panes.py
def mouseReleaseEvent(self, mouseEvent):
    """Called whenever a user have had a mouse button pressed, and
    releases it. We pass it through to the normal way Qt handles
    button pressed, but also sends as cursor movement signal to
    the device (except if a selection is made, for selections we first
    move the cursor on deselection)
    """
    super().mouseReleaseEvent(mouseEvent)

    # If when a user have clicked and not made a selection
    # move the device cursor to where the user clicked
    if not self.textCursor().hasSelection():
        self.set_devicecursor_to_qtcursor()

move_cursor_to(new_position)

Move the cursor, by sending vt100 left/right signals through serial. The Qt cursor is first returned to the known location of the device cursor. Then the appropriate number of move left or right signals are send. The Qt cursor is not moved to the new_position here, but will be moved once receiving a response (in process_tty_data).

Source code in mu/interface/panes.py
def move_cursor_to(self, new_position):
    """Move the cursor, by sending vt100 left/right signals through
    serial. The Qt cursor is first returned to the known location
    of the device cursor.  Then the appropriate number of move
    left or right signals are send.  The Qt cursor is not moved to
    the new_position here, but will be moved once receiving a
    response (in process_tty_data).
    """
    # Reset Qt cursor position
    self.set_qtcursor_to_devicecursor()
    # Calculate number of steps
    steps = new_position - self.device_cursor_position

    # Limit cursor movement to the last line
    if steps < 0:
        cursor = self.textCursor()
        cursor.setPosition(self.device_cursor_position)
        cursor.movePosition(
            QTextCursor.MoveOperation.StartOfLine,
            mode=QTextCursor.MoveMode.KeepAnchor,
        )
        line_start = cursor.selectionStart()
        if line_start > new_position:
            return

    # Send the appropriate right/left moves
    if steps > 0:
        # Move cursor right if positive
        self.send(VT100_RIGHT * steps)
    elif steps < 0:
        # Move cursor left if negative
        self.send(VT100_LEFT * abs(steps))

process_tty_data(data)

Given some incoming bytes of data, work out how to handle / display them in the REPL widget. If received input is incomplete, stores remainder in self.unprocessed_input.

Updates the self.device_cursor_position to match that of the device for every input received.

Source code in mu/interface/panes.py
def process_tty_data(self, data):
    """
    Given some incoming bytes of data, work out how to handle / display
    them in the REPL widget.
    If received input is incomplete, stores remainder in
    self.unprocessed_input.

    Updates the self.device_cursor_position to match that of the device
    for every input received.
    """
    i = 0
    data = self.decoder.decode(data)
    if len(self.unprocessed_input) > 0:
        # Prepend bytes from last time, that wasn't processed
        data = self.unprocessed_input + data
        self.unprocessed_input = ""

    # Reset cursor. E.g. if doing a selection, the qt cursor and
    # device cursor will not match, we reset it here to make sure
    # they do match (this removes any selections when new input is
    # received)
    self.set_qtcursor_to_devicecursor()
    tc = self.textCursor()

    while i < len(data):
        if data[i] == "\b":
            tc.movePosition(QTextCursor.MoveOperation.Left)
            self.device_cursor_position = tc.position()
        elif data[i] == "\r":
            # Carriage return. Do nothing, we handle newlines when
            # reading \n
            pass
        elif data[i] == "\x1b":
            # Escape
            if len(data) > i + 1 and data[i + 1] == "[":
                # VT100 cursor detected: <Esc>[
                match = self.vt100_regex.search(data[i:])
                if match:
                    # move to (almost) after control seq
                    # (will ++ at end of loop)
                    i += match.end() - 1
                    count_string = match.group("count")
                    count = 1 if count_string == "" else int(count_string)
                    action = match.group("action")
                    if action == "A":  # up
                        tc.movePosition(QTextCursor.MoveOperation.Up, n=count)
                        self.device_cursor_position = tc.position()
                    elif action == "B":  # down
                        tc.movePosition(QTextCursor.MoveOperation.Down, n=count)
                        self.device_cursor_position = tc.position()
                    elif action == "C":  # right
                        tc.movePosition(QTextCursor.MoveOperation.Right, n=count)
                        self.device_cursor_position = tc.position()
                    elif action == "D":  # left
                        tc.movePosition(QTextCursor.MoveOperation.Left, n=count)
                        self.device_cursor_position = tc.position()
                    elif action == "K":  # delete things
                        if count_string == "":  # delete to end of line
                            tc.movePosition(
                                QTextCursor.MoveOperation.EndOfLine,
                                mode=QTextCursor.MoveMode.KeepAnchor,
                            )
                            tc.removeSelectedText()
                            self.device_cursor_position = tc.position()
                    else:
                        # Unknown action, log warning and ignore
                        command = match.group(0).replace("\x1b", "<Esc>")
                        msg = "Received unsupported VT100 command: {}"
                        logger.warning(msg.format(command))
                else:
                    # Cursor detected, but no match, must be
                    # incomplete input
                    self.unprocessed_input = data[i:]
                    break
            elif len(data) > i + 1 and data[i + 1] == "]":
                # OSC cursor detected: <Esc>]
                match = self.osc_regex.search(data[i:])
                if match:
                    # move to (almost) after control seq
                    # (will ++ at end of loop)
                    i += match.end() - 1
                    command = match.group("command")
                    string = match.group("string")
                    if command == "0":  # Set window title and icon name
                        logger.warning("dropped title {}".format(string))
                    else:
                        # Unknown action, log warning and ignore
                        command = match.group(0).replace("\x1b", "<Esc>")
                        msg = "Received unsupported VT100 command: {}"
                        logger.warning(msg.format(command))
                else:
                    # Cursor detected, but no match, must be
                    # incomplete input
                    self.unprocessed_input = data[i:]
                    break
            elif len(data) == i + 1:
                # Escape received as end of transmission. Perhaps
                # the transmission is incomplete, wait until next
                # bytes are received to determine what to do
                self.unprocessed_input = data[i:]
                break
        elif data[i] == "\n":
            tc.movePosition(QTextCursor.MoveOperation.End)
            self.device_cursor_position = tc.position() + 1
            self.setTextCursor(tc)
            self.insertPlainText(data[i])
        else:
            # Char received, with VT100 that should be interpreted
            # as overwrite the char in front of the cursor
            tc.deleteChar()
            self.device_cursor_position = tc.position() + 1
            self.insertPlainText(data[i])
        self.setTextCursor(tc)
        i += 1
    # Scroll textarea if necessary to see cursor
    self.ensureCursorVisible()

set_devicecursor_to_qtcursor()

Call this whenever the cursor has been moved by the user, to send the cursor movement to the device.

Source code in mu/interface/panes.py
def set_devicecursor_to_qtcursor(self):
    """
    Call this whenever the cursor has been moved by the user, to send
    the cursor movement to the device.
    """
    self.move_cursor_to(self.textCursor().position())

set_font_size(new_size=DEFAULT_FONT_SIZE)

Sets the font size for all the textual elements in this pane.

Source code in mu/interface/panes.py
def set_font_size(self, new_size=DEFAULT_FONT_SIZE):
    """
    Sets the font size for all the textual elements in this pane.
    """
    self.font_size = new_size
    font = self.font()
    font.setPointSize(new_size)
    self.setFont(font)

set_qtcursor_to_devicecursor()

Resets the Qt TextCursor to where we know the device has the cursor placed.

Source code in mu/interface/panes.py
def set_qtcursor_to_devicecursor(self):
    """
    Resets the Qt TextCursor to where we know the device has the cursor
    placed.
    """
    tc = self.textCursor()
    tc.setPosition(self.device_cursor_position)
    self.setTextCursor(tc)

set_zoom(size)

Set the current zoom level given the "t-shirt" size.

Source code in mu/interface/panes.py
def set_zoom(self, size):
    """
    Set the current zoom level given the "t-shirt" size.
    """
    self.set_font_size(PANE_ZOOM_SIZES[size])

PlotterPane

Bases: QChartView

This plotter widget makes viewing sensor data easy!

This widget represents a chart that will look for tuple data from the MicroPython REPL, Python 3 REPL or Python 3 code runner and will auto-generate a graph.

Source code in mu/interface/panes.py
class PlotterPane(QChartView):
    """
    This plotter widget makes viewing sensor data easy!

    This widget represents a chart that will look for tuple data from
    the MicroPython REPL, Python 3 REPL or Python 3 code runner and will
    auto-generate a graph.
    """

    data_flood = pyqtSignal()

    def __init__(self, parent=None):
        super().__init__(parent)
        # Holds the raw input to be checked for actionable data to display.
        self.input_buffer = []
        # Holds the raw actionable data detected while plotting.
        self.raw_data = []
        self.setObjectName("plotterpane")
        # Number of datapoints to show (caps at self.max_x)
        self.num_datapoints = 0
        self.lookback = 500
        self.max_x = 100  # Maximum value along x axis
        self.max_y = 1000  # Maximum value +/- along y axis
        self.min_y = -1000
        self.flooded = False  # Flag to indicate if data flooding is happening.

        # Holds dequeues for each slot of incoming data (assumes 1 to start with)
        self.data = [deque([0] * self.lookback)]
        # Holds line series for each slot of incoming data (assumes 1 to start
        # with).
        self.series = [QLineSeries()]

        # Ranges used for the Y axis (up to 1000, after which we just double
        # the range).
        self.y_ranges = [0, 1, 5, 10, 25, 50, 100, 250, 500, 1000]

        # Set up the chart with sensible defaults.
        self.chart = QChart()
        self.chart.legend().hide()
        self.chart.addSeries(self.series[0])
        self.axis_x = QValueAxis()
        self.axis_y = QValueAxis()
        self.axis_x.setRange(0, self.max_x)
        self.axis_y.setRange(self.min_y, self.max_y)
        self.axis_x.setLabelFormat("time")
        self.axis_y.setLabelFormat("%d")
        # Qt6 dropped QChart.setAxisX/setAxisY: axes are added to the chart and
        # then attached to each series.
        self.chart.addAxis(self.axis_x, Qt.AlignmentFlag.AlignBottom)
        self.chart.addAxis(self.axis_y, Qt.AlignmentFlag.AlignLeft)
        self.series[0].attachAxis(self.axis_x)
        self.series[0].attachAxis(self.axis_y)
        self.setChart(self.chart)
        self.setRenderHint(QPainter.RenderHint.Antialiasing)

    def process_tty_data(self, data):
        """
        Takes raw bytes and, if a valid tuple is detected, adds the data to
        the plotter.

        The the length of the bytes data > 1024 then a data_flood signal is
        emitted to ensure Mu can take action to remain responsive.
        """
        # Data flooding guards.
        if self.flooded:
            return
        if len(data) > 1024:
            self.flooded = True
            self.data_flood.emit()
            return
        data = data.replace(b"\r\n", b"\n")
        self.input_buffer.append(data)
        # Check if the data contains a Python tuple, containing numbers, on a
        # single line (i.e. ends with \n).
        input_bytes = b"".join(self.input_buffer)
        lines = input_bytes.split(b"\n")
        for line in lines:
            if line.startswith(b"(") and line.endswith(b")"):
                # Candidate tuple. Extract the raw bytes into a numeric tuple.
                raw_values = [val.strip() for val in line[1:-1].split(b",")]
                numeric_values = []
                for raw in raw_values:
                    try:
                        numeric_values.append(int(raw))
                        # It worked, so move onto the next value.
                        continue
                    except ValueError:
                        # Try again as a float.
                        pass
                    try:
                        numeric_values.append(float(raw))
                    except ValueError:
                        # Not an int or float, so ignore this value.
                        continue
                if numeric_values:
                    # There were numeric values in the tuple, so use them!
                    self.add_data(tuple(numeric_values))
        # Reset the input buffer.
        self.input_buffer = []
        if lines[-1]:
            # Append any bytes that are not yet at the end of a line, for
            # processing next time we read data from self.connection.
            self.input_buffer.append(lines[-1])

    def add_data(self, values):
        """
        Given a tuple of values, ensures there are the required number of line
        series, add the data to the line series, update the range of the chart
        so the chart displays nicely.
        """
        # Store incoming data to dump as CSV at the end of the session.
        self.raw_data.append(values)
        # Check the number of incoming values.
        if len(values) != len(self.series):
            # Adjust the number of line series.
            value_len = len(values)
            series_len = len(self.series)
            if value_len > series_len:
                # Add new line series.
                for i in range(value_len - series_len):
                    new_series = QLineSeries()
                    self.chart.addSeries(new_series)
                    new_series.attachAxis(self.axis_x)
                    new_series.attachAxis(self.axis_y)
                    self.series.append(new_series)
                    self.data.append(deque([0] * self.lookback))
            else:
                # Remove old line series.
                for old_series in self.series[value_len:]:
                    self.chart.removeSeries(old_series)
                self.series = self.series[:value_len]
                self.data = self.data[:value_len]

        # Add the incoming values to the data to be displayed, and compute
        # max range.
        max_ranges = []
        min_ranges = []
        for i, value in enumerate(values):
            self.data[i].appendleft(value)
            max_ranges.append(max(self.data[i]))
            min_ranges.append(min(self.data[i]))
            if len(self.data[i]) > self.lookback:
                self.data[i].pop()
            self.num_datapoints = min(self.num_datapoints + 1, self.max_x)

        # Re-scale y-axis.
        max_y_range = max(max_ranges)
        y_range = bisect.bisect_left(self.y_ranges, max_y_range)
        if y_range < len(self.y_ranges):
            self.max_y = self.y_ranges[y_range]
        elif max_y_range > self.max_y:
            self.max_y += self.max_y
        elif max_y_range < self.max_y / 2:
            self.max_y = self.max_y / 2

        min_y_range = min(min_ranges)
        y_range = bisect.bisect_left(self.y_ranges, abs(min_y_range))
        if y_range < len(self.y_ranges):
            self.min_y = -self.y_ranges[y_range]
        elif min_y_range < self.min_y:
            self.min_y += self.min_y
        elif min_y_range > self.min_y / 2:
            self.min_y = self.min_y / 2

        self.axis_y.setRange(self.min_y, self.max_y)

        # Ensure floats are used to label y axis if the range is small.
        if self.max_y - self.min_y <= 10:
            self.axis_y.setLabelFormat("%2.2f")
        else:
            self.axis_y.setLabelFormat("%d")

        # Update the line series with the data.
        for i, line_series in enumerate(self.series):
            line_series.clear()
            xy_vals = []
            for j in range(self.num_datapoints):
                val = self.data[i][self.num_datapoints - 1 - j]
                xy_vals.append((j, val))
            for point in xy_vals:
                line_series.append(*point)

    def set_theme(self, theme):
        """
        Sets the theme / look for the plotter pane.
        """
        if theme == "day":
            self.chart.setTheme(QChart.ChartTheme.ChartThemeLight)
        elif theme == "night":
            self.chart.setTheme(QChart.ChartTheme.ChartThemeDark)
        else:
            self.chart.setTheme(QChart.ChartTheme.ChartThemeHighContrast)

add_data(values)

Given a tuple of values, ensures there are the required number of line series, add the data to the line series, update the range of the chart so the chart displays nicely.

Source code in mu/interface/panes.py
def add_data(self, values):
    """
    Given a tuple of values, ensures there are the required number of line
    series, add the data to the line series, update the range of the chart
    so the chart displays nicely.
    """
    # Store incoming data to dump as CSV at the end of the session.
    self.raw_data.append(values)
    # Check the number of incoming values.
    if len(values) != len(self.series):
        # Adjust the number of line series.
        value_len = len(values)
        series_len = len(self.series)
        if value_len > series_len:
            # Add new line series.
            for i in range(value_len - series_len):
                new_series = QLineSeries()
                self.chart.addSeries(new_series)
                new_series.attachAxis(self.axis_x)
                new_series.attachAxis(self.axis_y)
                self.series.append(new_series)
                self.data.append(deque([0] * self.lookback))
        else:
            # Remove old line series.
            for old_series in self.series[value_len:]:
                self.chart.removeSeries(old_series)
            self.series = self.series[:value_len]
            self.data = self.data[:value_len]

    # Add the incoming values to the data to be displayed, and compute
    # max range.
    max_ranges = []
    min_ranges = []
    for i, value in enumerate(values):
        self.data[i].appendleft(value)
        max_ranges.append(max(self.data[i]))
        min_ranges.append(min(self.data[i]))
        if len(self.data[i]) > self.lookback:
            self.data[i].pop()
        self.num_datapoints = min(self.num_datapoints + 1, self.max_x)

    # Re-scale y-axis.
    max_y_range = max(max_ranges)
    y_range = bisect.bisect_left(self.y_ranges, max_y_range)
    if y_range < len(self.y_ranges):
        self.max_y = self.y_ranges[y_range]
    elif max_y_range > self.max_y:
        self.max_y += self.max_y
    elif max_y_range < self.max_y / 2:
        self.max_y = self.max_y / 2

    min_y_range = min(min_ranges)
    y_range = bisect.bisect_left(self.y_ranges, abs(min_y_range))
    if y_range < len(self.y_ranges):
        self.min_y = -self.y_ranges[y_range]
    elif min_y_range < self.min_y:
        self.min_y += self.min_y
    elif min_y_range > self.min_y / 2:
        self.min_y = self.min_y / 2

    self.axis_y.setRange(self.min_y, self.max_y)

    # Ensure floats are used to label y axis if the range is small.
    if self.max_y - self.min_y <= 10:
        self.axis_y.setLabelFormat("%2.2f")
    else:
        self.axis_y.setLabelFormat("%d")

    # Update the line series with the data.
    for i, line_series in enumerate(self.series):
        line_series.clear()
        xy_vals = []
        for j in range(self.num_datapoints):
            val = self.data[i][self.num_datapoints - 1 - j]
            xy_vals.append((j, val))
        for point in xy_vals:
            line_series.append(*point)

process_tty_data(data)

Takes raw bytes and, if a valid tuple is detected, adds the data to the plotter.

The the length of the bytes data > 1024 then a data_flood signal is emitted to ensure Mu can take action to remain responsive.

Source code in mu/interface/panes.py
def process_tty_data(self, data):
    """
    Takes raw bytes and, if a valid tuple is detected, adds the data to
    the plotter.

    The the length of the bytes data > 1024 then a data_flood signal is
    emitted to ensure Mu can take action to remain responsive.
    """
    # Data flooding guards.
    if self.flooded:
        return
    if len(data) > 1024:
        self.flooded = True
        self.data_flood.emit()
        return
    data = data.replace(b"\r\n", b"\n")
    self.input_buffer.append(data)
    # Check if the data contains a Python tuple, containing numbers, on a
    # single line (i.e. ends with \n).
    input_bytes = b"".join(self.input_buffer)
    lines = input_bytes.split(b"\n")
    for line in lines:
        if line.startswith(b"(") and line.endswith(b")"):
            # Candidate tuple. Extract the raw bytes into a numeric tuple.
            raw_values = [val.strip() for val in line[1:-1].split(b",")]
            numeric_values = []
            for raw in raw_values:
                try:
                    numeric_values.append(int(raw))
                    # It worked, so move onto the next value.
                    continue
                except ValueError:
                    # Try again as a float.
                    pass
                try:
                    numeric_values.append(float(raw))
                except ValueError:
                    # Not an int or float, so ignore this value.
                    continue
            if numeric_values:
                # There were numeric values in the tuple, so use them!
                self.add_data(tuple(numeric_values))
    # Reset the input buffer.
    self.input_buffer = []
    if lines[-1]:
        # Append any bytes that are not yet at the end of a line, for
        # processing next time we read data from self.connection.
        self.input_buffer.append(lines[-1])

set_theme(theme)

Sets the theme / look for the plotter pane.

Source code in mu/interface/panes.py
def set_theme(self, theme):
    """
    Sets the theme / look for the plotter pane.
    """
    if theme == "day":
        self.chart.setTheme(QChart.ChartTheme.ChartThemeLight)
    elif theme == "night":
        self.chart.setTheme(QChart.ChartTheme.ChartThemeDark)
    else:
        self.chart.setTheme(QChart.ChartTheme.ChartThemeHighContrast)

PythonProcessPane

Bases: QTextEdit

Handles / displays a Python process's stdin/out with working command history and simple buffer editing.

Source code in mu/interface/panes.py
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
class PythonProcessPane(QTextEdit):
    """
    Handles / displays a Python process's stdin/out with working command
    history and simple buffer editing.
    """

    on_append_text = pyqtSignal(bytes)

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setFont(Font().load())
        self.font_size = DEFAULT_FONT_SIZE
        self.setAcceptRichText(False)
        self.setReadOnly(False)
        self.setUndoRedoEnabled(False)
        self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
        self.customContextMenuRequested.connect(self.context_menu)
        self.running = False  # Flag to show the child process is running.
        self.setObjectName("PythonRunner")
        self.process = None  # Will eventually reference the running process.
        self.input_history = []  # history of inputs entered in this session.
        self.start_of_current_line = 0  # start position of the input line.
        self.history_position = 0  # current position when navigation history.
        self.stdout_buffer = b""  # contains non-decoded bytes from stdout.
        self.reading_stdout = False  # flag showing if already reading stdout.
        self.is_interactive = False  # flag if the process is interactive mode.

    def start_process(
        self,
        interpreter,
        script_name,
        working_directory,
        interactive=True,
        debugger=False,
        command_args=None,
        envars=None,
        python_args=None,
    ):
        """
        Start the child Python process.

        Will use the referenced interpreter to run the Python
        script_name within the context of the working directory.

        If interactive is True (the default) the Python process will run in
        interactive mode (dropping the user into the REPL when the script
        completes).

        If debugger is True (the default is False) then the script will run
        within a debug runner session.

        If there is a list of command_args (the default is None), then these
        will be passed as further arguments into the script to be run.

        If there is a list of environment variables, these will be part of the
        context of the new child process.

        If python_args is given, these are passed as arguments to the Python
        interpreter used to launch the child process.
        """
        self.is_interactive = interactive
        if not envars:  # Envars must be a dict if not passed a value.
            envars = {}
        envars = {name: v for (name, v) in envars.items() if name != "PYTHONPATH"}
        self.script = ""
        if script_name:
            self.script = os.path.abspath(os.path.normcase(script_name))
        logger.info("Running script: {}".format(self.script))
        logger.info("Using interpreter: {}".format(interpreter))
        if interactive:
            logger.info("Running with interactive mode.")
        if command_args is None:
            command_args = []
        logger.info("Command args: {}".format(command_args))
        self.process = QProcess(self)
        self.process.setProcessChannelMode(QProcess.ProcessChannelMode.MergedChannels)
        # Force buffers to flush immediately.
        env = QProcessEnvironment.systemEnvironment()
        env.insert("PYTHONUNBUFFERED", "1")
        env.insert("PYTHONIOENCODING", "utf-8")
        if sys.platform == "darwin":
            # Ensure the correct encoding is set for the environment. If the
            # following two lines are not set, then Flask will complain about
            # Python 3 being misconfigured to use ASCII encoding.
            # See: https://click.palletsprojects.com/en/7.x/python3/
            encoding = "{}.utf-8".format(language_code)
            env.insert("LC_ALL", encoding)
            env.insert("LANG", encoding)
        # Activate the user's virtual environment for the child process: point
        # VIRTUAL_ENV at it and prepend its bin/Scripts dir to PATH. The script
        # is already launched with the venv interpreter; this makes the rest of
        # the environment match it (tools resolved by name, VIRTUAL_ENV-aware
        # libraries). User-supplied envars below still take precedence.
        for name, value in venv.activation_vars().items():
            env.insert(name, value)
        # Manage environment variables that may have been set by the user.
        if envars:
            logger.info("Running with environment variables: {}".format(envars))
            for name, value in envars.items():
                env.insert(name, value)
        logger.info("Working directory: {}".format(working_directory))
        self.process.setWorkingDirectory(working_directory)
        self.process.readyRead.connect(self.try_read_from_stdout)
        self.process.finished.connect(self.finished)
        logger.info("Python path: {}".format(sys.path))
        if debugger:
            # Start the mu_debug runner for the script.
            parent_dir = os.path.join(os.path.dirname(__file__), "..")
            mu_dir = os.path.abspath(parent_dir)
            logger.info("mu_dir: %s", mu_dir)
            runner = os.path.join(mu_dir, "mu_debug.py")
            args = [runner, self.script] + command_args
            #
            # The runtime virtualenvironment doesn't include Mu
            # itself (by design). But the debugger needs mu in
            # order to run, so we temporarily set the PYTHONPATH
            # to point to Mu's own directory
            #
            env.insert("PYTHONPATH", os.path.abspath(os.path.join(mu_dir, "..")))
            self.process.setProcessEnvironment(env)
            self.process.start(interpreter, args)
        else:
            args = []
            if self.script:
                if interactive:
                    # Start the script in interactive Python mode.
                    args = ["-i", self.script] + command_args
                else:
                    # Just run the command with no additional flags.
                    args = [self.script] + command_args
            if python_args:
                args = python_args + args
            logger.info("Args: {}".format(args))
            self.process.setProcessEnvironment(env)
            self.process.start(interpreter, args)
            self.running = True

    def stop_process(self):
        if self.process:
            self.process.terminate()
            terminated = self.process.waitForFinished(10)
            if not terminated:
                self.process.kill()
                self.process.waitForFinished()
            self.running = False

    def _del_(self):
        self.stop_process()

    def finished(self, code, status):
        """
        Handle when the child process finishes.
        """
        self.running = False
        cursor = self.textCursor()
        cursor.movePosition(cursor.End)
        cursor.insertText("\n\n---------- FINISHED ----------\n")
        msg = "exit code: {} status: {}".format(code, status)
        cursor.insertText(msg)
        cursor.movePosition(QTextCursor.MoveOperation.End)
        self.setTextCursor(cursor)
        self.setReadOnly(True)

    def context_menu(self):
        """
        Creates custom context menu with just copy and paste.
        """
        menu = QMenu(self)
        if platform.system() == "Darwin":
            copy_keys = QKeySequence(Qt.Modifier.CTRL | Qt.Key.Key_C)
            paste_keys = QKeySequence(Qt.Modifier.CTRL | Qt.Key.Key_V)
        else:
            copy_keys = QKeySequence(
                Qt.Modifier.CTRL | Qt.Modifier.SHIFT | Qt.Key.Key_C
            )
            paste_keys = QKeySequence(
                Qt.Modifier.CTRL | Qt.Modifier.SHIFT | Qt.Key.Key_V
            )
        menu.addAction("Copy", self.copy, copy_keys)
        menu.addAction("Paste", self.paste, paste_keys)
        menu.exec(QCursor.pos())

    def insertFromMimeData(self, source):
        """
        Insert mime data by sending it to the REPL
        """
        if source and source.text():
            # normalize for Windows line-ends.
            text = "\n".join(source.text().splitlines())
            if text:
                self.parse_paste(text)

    def parse_paste(self, text):
        """
        Recursively takes characters from text to be parsed as input. We do
        this so the event loop has time to respond to output from the process
        to which the characters are sent (for example, when a newline is sent).

        Yes, this is a quick and dirty hack, but ensures the pasted input is
        also evaluated in an interactive manner rather than as a single-shot
        splurge of data. Essentially, it's simulating someone typing in the
        characters of the pasted text *really fast* but in such a way that the
        event loop cycles.
        """
        character = text[0]  # the current character to process.
        remainder = text[1:]  # remaining characters to process in the future.
        if character.isprintable() or character in string.printable:
            if character == "\n" or character == "\r":
                self.parse_input(Qt.Key.Key_Enter, character, None)
            else:
                self.parse_input(None, character, None)
        if remainder:
            # Schedule a recursive call of parse_paste with the remaining text
            # to process. This allows the event loop to cycle and handle any
            # output from the child process as a result of the text pasted so
            # far (especially useful for handling responses from newlines).
            QTimer.singleShot(2, lambda text=remainder: self.parse_paste(text))

    def keyPressEvent(self, data):
        """
        Called when the user types something in the REPL.
        """
        key = data.key()
        text = data.text()
        modifiers = data.modifiers()
        self.parse_input(key, text, modifiers)

    def on_process_halt(self):
        """
        Called when the the user has manually halted a running process. Ensures
        that the remaining data from the halted process's stdout is handled
        properly.

        When the process is halted the user is dropped into the Python prompt
        and this method ensures the UI is updated in a clean, non-blocking
        way.
        """
        data = self.process.readAll().data()
        if data:
            while True:
                try:
                    self.append(data)
                    self.on_append_text.emit(data)
                    self.set_start_of_current_line()
                    break
                except UnicodeDecodeError:
                    # Discard problematic start byte and try again.
                    # (This may be caused by a split in multi-byte characters).
                    data = data[1:]

    def parse_input(self, key, text, modifiers):
        """
        Correctly encodes user input and sends it to the connected process.

        The key is a Qt.Key.Key_Something value, text is the textual representation
        of the input, and modifiers are the control keys (shift, CTRL, META,
        etc) also used.
        """
        msg = b""  # Eventually to be inserted into the pane at the cursor.
        if key == Qt.Key.Key_Enter or key == Qt.Key.Key_Return:
            msg = b"\n"
        elif (
            platform.system() == "Darwin"
            and modifiers == Qt.KeyboardModifier.MetaModifier
        ) or (
            platform.system() != "Darwin"
            and modifiers == Qt.KeyboardModifier.ControlModifier
        ):
            # Handle CTRL-C and CTRL-D
            if self.process and self.running:
                pid = self.process.processId()
                # NOTE: Windows related constraints don't allow us to send a
                # CTRL-C, rather, the process will just terminate.
                halt_flag = False
                if key == Qt.Key.Key_C:
                    halt_flag = True
                    os.kill(pid, signal.SIGINT)
                if key == Qt.Key.Key_D:
                    halt_flag = True
                    self.process.kill()
                if halt_flag:
                    # Clean up from kill signal.
                    self.process.readAll()  # Discard queued output.
                    self.stdout_buffer = b""
                    # Schedule update of the UI after the process halts (in
                    # next iteration of the event loop).
                    QTimer.singleShot(1, self.on_process_halt)
                    return
        elif key == Qt.Key.Key_Up:
            self.history_back()
        elif key == Qt.Key.Key_Down:
            self.history_forward()
        elif key == Qt.Key.Key_Right:
            cursor = self.textCursor()
            cursor.movePosition(QTextCursor.MoveOperation.Right)
            self.setTextCursor(cursor)
        elif key == Qt.Key.Key_Left:
            cursor = self.textCursor()
            if cursor.position() > self.start_of_current_line:
                cursor.movePosition(QTextCursor.MoveOperation.Left)
                self.setTextCursor(cursor)
        elif key == Qt.Key.Key_Home:
            cursor = self.textCursor()
            cursor.movePosition(QTextCursor.MoveOperation.End)
            buffer_len = len(self.toPlainText()) - self.start_of_current_line
            for i in range(buffer_len):
                cursor.movePosition(QTextCursor.MoveOperation.Left)
            self.setTextCursor(cursor)
        elif key == Qt.Key.Key_End:
            cursor = self.textCursor()
            cursor.movePosition(QTextCursor.MoveOperation.End)
            self.setTextCursor(cursor)
        elif (
            modifiers
            == Qt.KeyboardModifier.ControlModifier | Qt.KeyboardModifier.ShiftModifier
        ) or (
            platform.system() == "Darwin"
            and modifiers == Qt.KeyboardModifier.ControlModifier
        ):
            # Command-key on Mac, Ctrl-Shift on Win/Lin
            if key == Qt.Key.Key_C:
                self.copy()
            elif key == Qt.Key.Key_V:
                self.paste()
        elif text.isprintable():
            # If the key is for a printable character then add it to the
            # active buffer and display it.
            msg = bytes(text, "utf8")
        if key == Qt.Key.Key_Backspace:
            self.backspace()
        if key == Qt.Key.Key_Delete:
            self.delete()
        if key == Qt.Key.Key_Enter or key == Qt.Key.Key_Return:
            # First move cursor to the end of the line and insert newline in
            # case return/enter is pressed while the cursor is in the
            # middle of the line
            cursor = self.textCursor()
            cursor.movePosition(QTextCursor.MoveOperation.End)
            self.setTextCursor(cursor)
            self.insert(msg)
            # Then write line to std_in and add to history
            content = self.toPlainText()
            line = content[self.start_of_current_line :].encode("utf-8")
            self.write_to_stdin(line)
            if line.strip():
                self.input_history.append(line.replace(b"\n", b""))
            self.history_position = 0
            self.set_start_of_current_line()
        elif not self.isReadOnly() and msg:
            self.insert(msg)

    def set_start_of_current_line(self):
        """
        Set the flag to indicate the start of the current line (used before
        waiting for input).

        This flag is used to discard the preceding text in the text entry
        field when Mu parses new input from the user (i.e. any text beyond the
        self.start_of_current_line).
        """
        self.start_of_current_line = len(self.toPlainText())

    def history_back(self):
        """
        Replace the current input line with the next item BACK from the
        current history position.
        """
        if self.input_history:
            self.history_position -= 1
            history_pos = len(self.input_history) + self.history_position
            if history_pos < 0:
                self.history_position += 1
                history_pos = 0
            history_item = self.input_history[history_pos]
            self.replace_input_line(history_item)

    def history_forward(self):
        """
        Replace the current input line with the next item FORWARD from the
        current history position.
        """
        if self.input_history:
            self.history_position += 1
            history_pos = len(self.input_history) + self.history_position
            if history_pos >= len(self.input_history):
                # At the most recent command.
                self.history_position = 0
                self.clear_input_line()
                return
            history_item = self.input_history[history_pos]
            self.replace_input_line(history_item)

    def try_read_from_stdout(self):
        """
        Ensure reading from stdout only happens if there is NOT already current
        attempts to read from stdout.
        """
        if not self.reading_stdout:
            self.reading_stdout = True
            self.read_from_stdout()

    def read_from_stdout(self):
        """
        Process incoming data from the process's stdout.
        """
        data = self.process.read(256)
        if data:
            data = self.stdout_buffer + data
            try:
                self.append(data)
                self.on_append_text.emit(data)
                self.set_start_of_current_line()
                self.stdout_buffer = b""
            except UnicodeDecodeError:
                self.stdout_buffer = data
            QTimer.singleShot(2, self.read_from_stdout)
        else:
            self.reading_stdout = False

    def write_to_stdin(self, data):
        """
        Writes data from the Qt application to the child process's stdin.
        """
        if self.process:
            self.process.write(data)

    def append(self, msg):
        """
        Append text to the text area.
        """
        cursor = self.textCursor()
        cursor.movePosition(QTextCursor.MoveOperation.End)
        cursor.insertText(msg.decode("utf-8"))
        cursor.movePosition(QTextCursor.MoveOperation.End)
        self.setTextCursor(cursor)

    def insert(self, msg):
        """
        Insert text to the text area at the current cursor position.
        """
        cursor = self.textCursor()
        if cursor.position() < self.start_of_current_line:
            cursor.movePosition(QTextCursor.MoveOperation.End)
        cursor.insertText(msg.decode("utf-8"))
        self.setTextCursor(cursor)

    def backspace(self):
        """
        Removes a character from the current buffer -- to the left of cursor.
        """
        cursor = self.textCursor()
        if cursor.position() > self.start_of_current_line:
            cursor = self.textCursor()
            cursor.deletePreviousChar()
            self.setTextCursor(cursor)

    def delete(self):
        """
        Removes a character from the current buffer -- to the right of cursor.
        """
        cursor = self.textCursor()
        if cursor.position() >= self.start_of_current_line:
            cursor.deleteChar()
            self.setTextCursor(cursor)

    def clear_input_line(self):
        """
        Remove all the characters currently in the input buffer line.
        """
        cursor = self.textCursor()
        cursor.movePosition(QTextCursor.MoveOperation.End)
        buffer_len = len(self.toPlainText()) - self.start_of_current_line
        for i in range(buffer_len):
            cursor.deletePreviousChar()
        self.setTextCursor(cursor)

    def replace_input_line(self, text):
        """
        Replace the current input line with the passed in text.
        """
        self.clear_input_line()
        self.append(text)

    def set_font_size(self, new_size=DEFAULT_FONT_SIZE):
        """
        Sets the font size for all the textual elements in this pane.
        """
        self.font_size = new_size
        f = self.font()
        f.setPointSize(new_size)
        self.setFont(f)

    def set_zoom(self, size):
        """
        Set the current zoom level given the "t-shirt" size.
        """
        self.set_font_size(PANE_ZOOM_SIZES[size])

    def set_theme(self, theme):
        self.set_font_size(self.font_size)

append(msg)

Append text to the text area.

Source code in mu/interface/panes.py
def append(self, msg):
    """
    Append text to the text area.
    """
    cursor = self.textCursor()
    cursor.movePosition(QTextCursor.MoveOperation.End)
    cursor.insertText(msg.decode("utf-8"))
    cursor.movePosition(QTextCursor.MoveOperation.End)
    self.setTextCursor(cursor)

backspace()

Removes a character from the current buffer -- to the left of cursor.

Source code in mu/interface/panes.py
def backspace(self):
    """
    Removes a character from the current buffer -- to the left of cursor.
    """
    cursor = self.textCursor()
    if cursor.position() > self.start_of_current_line:
        cursor = self.textCursor()
        cursor.deletePreviousChar()
        self.setTextCursor(cursor)

clear_input_line()

Remove all the characters currently in the input buffer line.

Source code in mu/interface/panes.py
def clear_input_line(self):
    """
    Remove all the characters currently in the input buffer line.
    """
    cursor = self.textCursor()
    cursor.movePosition(QTextCursor.MoveOperation.End)
    buffer_len = len(self.toPlainText()) - self.start_of_current_line
    for i in range(buffer_len):
        cursor.deletePreviousChar()
    self.setTextCursor(cursor)

context_menu()

Creates custom context menu with just copy and paste.

Source code in mu/interface/panes.py
def context_menu(self):
    """
    Creates custom context menu with just copy and paste.
    """
    menu = QMenu(self)
    if platform.system() == "Darwin":
        copy_keys = QKeySequence(Qt.Modifier.CTRL | Qt.Key.Key_C)
        paste_keys = QKeySequence(Qt.Modifier.CTRL | Qt.Key.Key_V)
    else:
        copy_keys = QKeySequence(
            Qt.Modifier.CTRL | Qt.Modifier.SHIFT | Qt.Key.Key_C
        )
        paste_keys = QKeySequence(
            Qt.Modifier.CTRL | Qt.Modifier.SHIFT | Qt.Key.Key_V
        )
    menu.addAction("Copy", self.copy, copy_keys)
    menu.addAction("Paste", self.paste, paste_keys)
    menu.exec(QCursor.pos())

delete()

Removes a character from the current buffer -- to the right of cursor.

Source code in mu/interface/panes.py
def delete(self):
    """
    Removes a character from the current buffer -- to the right of cursor.
    """
    cursor = self.textCursor()
    if cursor.position() >= self.start_of_current_line:
        cursor.deleteChar()
        self.setTextCursor(cursor)

finished(code, status)

Handle when the child process finishes.

Source code in mu/interface/panes.py
def finished(self, code, status):
    """
    Handle when the child process finishes.
    """
    self.running = False
    cursor = self.textCursor()
    cursor.movePosition(cursor.End)
    cursor.insertText("\n\n---------- FINISHED ----------\n")
    msg = "exit code: {} status: {}".format(code, status)
    cursor.insertText(msg)
    cursor.movePosition(QTextCursor.MoveOperation.End)
    self.setTextCursor(cursor)
    self.setReadOnly(True)

history_back()

Replace the current input line with the next item BACK from the current history position.

Source code in mu/interface/panes.py
def history_back(self):
    """
    Replace the current input line with the next item BACK from the
    current history position.
    """
    if self.input_history:
        self.history_position -= 1
        history_pos = len(self.input_history) + self.history_position
        if history_pos < 0:
            self.history_position += 1
            history_pos = 0
        history_item = self.input_history[history_pos]
        self.replace_input_line(history_item)

history_forward()

Replace the current input line with the next item FORWARD from the current history position.

Source code in mu/interface/panes.py
def history_forward(self):
    """
    Replace the current input line with the next item FORWARD from the
    current history position.
    """
    if self.input_history:
        self.history_position += 1
        history_pos = len(self.input_history) + self.history_position
        if history_pos >= len(self.input_history):
            # At the most recent command.
            self.history_position = 0
            self.clear_input_line()
            return
        history_item = self.input_history[history_pos]
        self.replace_input_line(history_item)

insert(msg)

Insert text to the text area at the current cursor position.

Source code in mu/interface/panes.py
def insert(self, msg):
    """
    Insert text to the text area at the current cursor position.
    """
    cursor = self.textCursor()
    if cursor.position() < self.start_of_current_line:
        cursor.movePosition(QTextCursor.MoveOperation.End)
    cursor.insertText(msg.decode("utf-8"))
    self.setTextCursor(cursor)

insertFromMimeData(source)

Insert mime data by sending it to the REPL

Source code in mu/interface/panes.py
def insertFromMimeData(self, source):
    """
    Insert mime data by sending it to the REPL
    """
    if source and source.text():
        # normalize for Windows line-ends.
        text = "\n".join(source.text().splitlines())
        if text:
            self.parse_paste(text)

keyPressEvent(data)

Called when the user types something in the REPL.

Source code in mu/interface/panes.py
def keyPressEvent(self, data):
    """
    Called when the user types something in the REPL.
    """
    key = data.key()
    text = data.text()
    modifiers = data.modifiers()
    self.parse_input(key, text, modifiers)

on_process_halt()

Called when the the user has manually halted a running process. Ensures that the remaining data from the halted process's stdout is handled properly.

When the process is halted the user is dropped into the Python prompt and this method ensures the UI is updated in a clean, non-blocking way.

Source code in mu/interface/panes.py
def on_process_halt(self):
    """
    Called when the the user has manually halted a running process. Ensures
    that the remaining data from the halted process's stdout is handled
    properly.

    When the process is halted the user is dropped into the Python prompt
    and this method ensures the UI is updated in a clean, non-blocking
    way.
    """
    data = self.process.readAll().data()
    if data:
        while True:
            try:
                self.append(data)
                self.on_append_text.emit(data)
                self.set_start_of_current_line()
                break
            except UnicodeDecodeError:
                # Discard problematic start byte and try again.
                # (This may be caused by a split in multi-byte characters).
                data = data[1:]

parse_input(key, text, modifiers)

Correctly encodes user input and sends it to the connected process.

The key is a Qt.Key.Key_Something value, text is the textual representation of the input, and modifiers are the control keys (shift, CTRL, META, etc) also used.

Source code in mu/interface/panes.py
def parse_input(self, key, text, modifiers):
    """
    Correctly encodes user input and sends it to the connected process.

    The key is a Qt.Key.Key_Something value, text is the textual representation
    of the input, and modifiers are the control keys (shift, CTRL, META,
    etc) also used.
    """
    msg = b""  # Eventually to be inserted into the pane at the cursor.
    if key == Qt.Key.Key_Enter or key == Qt.Key.Key_Return:
        msg = b"\n"
    elif (
        platform.system() == "Darwin"
        and modifiers == Qt.KeyboardModifier.MetaModifier
    ) or (
        platform.system() != "Darwin"
        and modifiers == Qt.KeyboardModifier.ControlModifier
    ):
        # Handle CTRL-C and CTRL-D
        if self.process and self.running:
            pid = self.process.processId()
            # NOTE: Windows related constraints don't allow us to send a
            # CTRL-C, rather, the process will just terminate.
            halt_flag = False
            if key == Qt.Key.Key_C:
                halt_flag = True
                os.kill(pid, signal.SIGINT)
            if key == Qt.Key.Key_D:
                halt_flag = True
                self.process.kill()
            if halt_flag:
                # Clean up from kill signal.
                self.process.readAll()  # Discard queued output.
                self.stdout_buffer = b""
                # Schedule update of the UI after the process halts (in
                # next iteration of the event loop).
                QTimer.singleShot(1, self.on_process_halt)
                return
    elif key == Qt.Key.Key_Up:
        self.history_back()
    elif key == Qt.Key.Key_Down:
        self.history_forward()
    elif key == Qt.Key.Key_Right:
        cursor = self.textCursor()
        cursor.movePosition(QTextCursor.MoveOperation.Right)
        self.setTextCursor(cursor)
    elif key == Qt.Key.Key_Left:
        cursor = self.textCursor()
        if cursor.position() > self.start_of_current_line:
            cursor.movePosition(QTextCursor.MoveOperation.Left)
            self.setTextCursor(cursor)
    elif key == Qt.Key.Key_Home:
        cursor = self.textCursor()
        cursor.movePosition(QTextCursor.MoveOperation.End)
        buffer_len = len(self.toPlainText()) - self.start_of_current_line
        for i in range(buffer_len):
            cursor.movePosition(QTextCursor.MoveOperation.Left)
        self.setTextCursor(cursor)
    elif key == Qt.Key.Key_End:
        cursor = self.textCursor()
        cursor.movePosition(QTextCursor.MoveOperation.End)
        self.setTextCursor(cursor)
    elif (
        modifiers
        == Qt.KeyboardModifier.ControlModifier | Qt.KeyboardModifier.ShiftModifier
    ) or (
        platform.system() == "Darwin"
        and modifiers == Qt.KeyboardModifier.ControlModifier
    ):
        # Command-key on Mac, Ctrl-Shift on Win/Lin
        if key == Qt.Key.Key_C:
            self.copy()
        elif key == Qt.Key.Key_V:
            self.paste()
    elif text.isprintable():
        # If the key is for a printable character then add it to the
        # active buffer and display it.
        msg = bytes(text, "utf8")
    if key == Qt.Key.Key_Backspace:
        self.backspace()
    if key == Qt.Key.Key_Delete:
        self.delete()
    if key == Qt.Key.Key_Enter or key == Qt.Key.Key_Return:
        # First move cursor to the end of the line and insert newline in
        # case return/enter is pressed while the cursor is in the
        # middle of the line
        cursor = self.textCursor()
        cursor.movePosition(QTextCursor.MoveOperation.End)
        self.setTextCursor(cursor)
        self.insert(msg)
        # Then write line to std_in and add to history
        content = self.toPlainText()
        line = content[self.start_of_current_line :].encode("utf-8")
        self.write_to_stdin(line)
        if line.strip():
            self.input_history.append(line.replace(b"\n", b""))
        self.history_position = 0
        self.set_start_of_current_line()
    elif not self.isReadOnly() and msg:
        self.insert(msg)

parse_paste(text)

Recursively takes characters from text to be parsed as input. We do this so the event loop has time to respond to output from the process to which the characters are sent (for example, when a newline is sent).

Yes, this is a quick and dirty hack, but ensures the pasted input is also evaluated in an interactive manner rather than as a single-shot splurge of data. Essentially, it's simulating someone typing in the characters of the pasted text really fast but in such a way that the event loop cycles.

Source code in mu/interface/panes.py
def parse_paste(self, text):
    """
    Recursively takes characters from text to be parsed as input. We do
    this so the event loop has time to respond to output from the process
    to which the characters are sent (for example, when a newline is sent).

    Yes, this is a quick and dirty hack, but ensures the pasted input is
    also evaluated in an interactive manner rather than as a single-shot
    splurge of data. Essentially, it's simulating someone typing in the
    characters of the pasted text *really fast* but in such a way that the
    event loop cycles.
    """
    character = text[0]  # the current character to process.
    remainder = text[1:]  # remaining characters to process in the future.
    if character.isprintable() or character in string.printable:
        if character == "\n" or character == "\r":
            self.parse_input(Qt.Key.Key_Enter, character, None)
        else:
            self.parse_input(None, character, None)
    if remainder:
        # Schedule a recursive call of parse_paste with the remaining text
        # to process. This allows the event loop to cycle and handle any
        # output from the child process as a result of the text pasted so
        # far (especially useful for handling responses from newlines).
        QTimer.singleShot(2, lambda text=remainder: self.parse_paste(text))

read_from_stdout()

Process incoming data from the process's stdout.

Source code in mu/interface/panes.py
def read_from_stdout(self):
    """
    Process incoming data from the process's stdout.
    """
    data = self.process.read(256)
    if data:
        data = self.stdout_buffer + data
        try:
            self.append(data)
            self.on_append_text.emit(data)
            self.set_start_of_current_line()
            self.stdout_buffer = b""
        except UnicodeDecodeError:
            self.stdout_buffer = data
        QTimer.singleShot(2, self.read_from_stdout)
    else:
        self.reading_stdout = False

replace_input_line(text)

Replace the current input line with the passed in text.

Source code in mu/interface/panes.py
def replace_input_line(self, text):
    """
    Replace the current input line with the passed in text.
    """
    self.clear_input_line()
    self.append(text)

set_font_size(new_size=DEFAULT_FONT_SIZE)

Sets the font size for all the textual elements in this pane.

Source code in mu/interface/panes.py
def set_font_size(self, new_size=DEFAULT_FONT_SIZE):
    """
    Sets the font size for all the textual elements in this pane.
    """
    self.font_size = new_size
    f = self.font()
    f.setPointSize(new_size)
    self.setFont(f)

set_start_of_current_line()

Set the flag to indicate the start of the current line (used before waiting for input).

This flag is used to discard the preceding text in the text entry field when Mu parses new input from the user (i.e. any text beyond the self.start_of_current_line).

Source code in mu/interface/panes.py
def set_start_of_current_line(self):
    """
    Set the flag to indicate the start of the current line (used before
    waiting for input).

    This flag is used to discard the preceding text in the text entry
    field when Mu parses new input from the user (i.e. any text beyond the
    self.start_of_current_line).
    """
    self.start_of_current_line = len(self.toPlainText())

set_zoom(size)

Set the current zoom level given the "t-shirt" size.

Source code in mu/interface/panes.py
def set_zoom(self, size):
    """
    Set the current zoom level given the "t-shirt" size.
    """
    self.set_font_size(PANE_ZOOM_SIZES[size])

start_process(interpreter, script_name, working_directory, interactive=True, debugger=False, command_args=None, envars=None, python_args=None)

Start the child Python process.

Will use the referenced interpreter to run the Python script_name within the context of the working directory.

If interactive is True (the default) the Python process will run in interactive mode (dropping the user into the REPL when the script completes).

If debugger is True (the default is False) then the script will run within a debug runner session.

If there is a list of command_args (the default is None), then these will be passed as further arguments into the script to be run.

If there is a list of environment variables, these will be part of the context of the new child process.

If python_args is given, these are passed as arguments to the Python interpreter used to launch the child process.

Source code in mu/interface/panes.py
def start_process(
    self,
    interpreter,
    script_name,
    working_directory,
    interactive=True,
    debugger=False,
    command_args=None,
    envars=None,
    python_args=None,
):
    """
    Start the child Python process.

    Will use the referenced interpreter to run the Python
    script_name within the context of the working directory.

    If interactive is True (the default) the Python process will run in
    interactive mode (dropping the user into the REPL when the script
    completes).

    If debugger is True (the default is False) then the script will run
    within a debug runner session.

    If there is a list of command_args (the default is None), then these
    will be passed as further arguments into the script to be run.

    If there is a list of environment variables, these will be part of the
    context of the new child process.

    If python_args is given, these are passed as arguments to the Python
    interpreter used to launch the child process.
    """
    self.is_interactive = interactive
    if not envars:  # Envars must be a dict if not passed a value.
        envars = {}
    envars = {name: v for (name, v) in envars.items() if name != "PYTHONPATH"}
    self.script = ""
    if script_name:
        self.script = os.path.abspath(os.path.normcase(script_name))
    logger.info("Running script: {}".format(self.script))
    logger.info("Using interpreter: {}".format(interpreter))
    if interactive:
        logger.info("Running with interactive mode.")
    if command_args is None:
        command_args = []
    logger.info("Command args: {}".format(command_args))
    self.process = QProcess(self)
    self.process.setProcessChannelMode(QProcess.ProcessChannelMode.MergedChannels)
    # Force buffers to flush immediately.
    env = QProcessEnvironment.systemEnvironment()
    env.insert("PYTHONUNBUFFERED", "1")
    env.insert("PYTHONIOENCODING", "utf-8")
    if sys.platform == "darwin":
        # Ensure the correct encoding is set for the environment. If the
        # following two lines are not set, then Flask will complain about
        # Python 3 being misconfigured to use ASCII encoding.
        # See: https://click.palletsprojects.com/en/7.x/python3/
        encoding = "{}.utf-8".format(language_code)
        env.insert("LC_ALL", encoding)
        env.insert("LANG", encoding)
    # Activate the user's virtual environment for the child process: point
    # VIRTUAL_ENV at it and prepend its bin/Scripts dir to PATH. The script
    # is already launched with the venv interpreter; this makes the rest of
    # the environment match it (tools resolved by name, VIRTUAL_ENV-aware
    # libraries). User-supplied envars below still take precedence.
    for name, value in venv.activation_vars().items():
        env.insert(name, value)
    # Manage environment variables that may have been set by the user.
    if envars:
        logger.info("Running with environment variables: {}".format(envars))
        for name, value in envars.items():
            env.insert(name, value)
    logger.info("Working directory: {}".format(working_directory))
    self.process.setWorkingDirectory(working_directory)
    self.process.readyRead.connect(self.try_read_from_stdout)
    self.process.finished.connect(self.finished)
    logger.info("Python path: {}".format(sys.path))
    if debugger:
        # Start the mu_debug runner for the script.
        parent_dir = os.path.join(os.path.dirname(__file__), "..")
        mu_dir = os.path.abspath(parent_dir)
        logger.info("mu_dir: %s", mu_dir)
        runner = os.path.join(mu_dir, "mu_debug.py")
        args = [runner, self.script] + command_args
        #
        # The runtime virtualenvironment doesn't include Mu
        # itself (by design). But the debugger needs mu in
        # order to run, so we temporarily set the PYTHONPATH
        # to point to Mu's own directory
        #
        env.insert("PYTHONPATH", os.path.abspath(os.path.join(mu_dir, "..")))
        self.process.setProcessEnvironment(env)
        self.process.start(interpreter, args)
    else:
        args = []
        if self.script:
            if interactive:
                # Start the script in interactive Python mode.
                args = ["-i", self.script] + command_args
            else:
                # Just run the command with no additional flags.
                args = [self.script] + command_args
        if python_args:
            args = python_args + args
        logger.info("Args: {}".format(args))
        self.process.setProcessEnvironment(env)
        self.process.start(interpreter, args)
        self.running = True

try_read_from_stdout()

Ensure reading from stdout only happens if there is NOT already current attempts to read from stdout.

Source code in mu/interface/panes.py
def try_read_from_stdout(self):
    """
    Ensure reading from stdout only happens if there is NOT already current
    attempts to read from stdout.
    """
    if not self.reading_stdout:
        self.reading_stdout = True
        self.read_from_stdout()

write_to_stdin(data)

Writes data from the Qt application to the child process's stdin.

Source code in mu/interface/panes.py
def write_to_stdin(self, data):
    """
    Writes data from the Qt application to the child process's stdin.
    """
    if self.process:
        self.process.write(data)

mu.interface.themes

Theme related code so Qt changes for each pre-defined theme.

mu.interface.themes

Theme and presentation related code for the Mu editor.

Copyright (c) 2015-2017 Nicholas H.Tollervey and others (see the AUTHORS file).

This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/.

ContrastTheme

Bases: Theme

Defines a Python related theme including the various font colours for syntax highlighting.

This is the high contrast theme.

Source code in mu/interface/themes.py
class ContrastTheme(Theme):
    """
    Defines a Python related theme including the various font colours for
    syntax highlighting.

    This is the high contrast theme.
    """

    FunctionMethodName = ClassName = Font(color="#AAA", paper="black")
    UnclosedString = Font(paper="#666")
    Comment = CommentBlock = Font(color="#AAA", paper="black")
    Keyword = Font(color="#EEE", bold=True, paper="black")
    SingleQuotedString = DoubleQuotedString = Font(color="#AAA", paper="black")
    SingleQuotedFString = DoubleQuotedFString = Font(color="#AAA", paper="black")
    TripleSingleQuotedString = TripleDoubleQuotedString = Font(
        color="#AAA", paper="black"
    )
    TripleSingleQuotedFString = TripleDoubleQuotedFString = Font(
        color="#AAA", paper="black"
    )
    Number = Font(color="#AAA", paper="black")
    Decorator = Font(color="#cccccc", paper="black")
    Default = Identifier = Font(color="#fff", paper="black")
    Operator = Font(color="#CCC", paper="black")
    HighlightedIdentifier = Font(color="#ffffff", paper="black")
    Paper = QColor("black")
    Caret = QColor("white")
    Margin = QColor("#333")
    IndicatorError = QColor("white")
    IndicatorStyle = QColor("cyan")
    DebugStyle = QColor("#666")
    IndicatorWordMatch = QColor("grey")
    BraceBackground = QColor("white")
    BraceForeground = QColor("black")
    UnmatchedBraceBackground = QColor("#666")
    UnmatchedBraceForeground = QColor("black")
    BreakpointMarker = QColor("lightGrey")
    # HTML
    Tag = Keyword
    UnknownTag = Tag
    XMLTagEnd = Tag
    XMLStart = Tag
    XMLEnd = Tag
    Attribute = ClassName
    UnknownAttribute = Attribute
    HTMLNumber = Number
    HTMLDoubleQuotedString = DoubleQuotedString
    HTMLSingleQuotedString = SingleQuotedString
    OtherInTag = Default
    HTMLComment = Comment
    Entity = Operator
    CDATA = Decorator
    # CSS
    ClassSelector = Tag
    PseudoClass = ClassSelector
    UnknownPseudoClass = ClassSelector
    CSS1Property = CSS2Property = CSS3Property = UnknownProperty = SingleQuotedString
    Value = Number
    IDSelector = Tag
    Important = UnmatchedBraceBackground
    AtRule = Decorator
    MediaRule = Decorator
    Variable = HighlightedIdentifier

DayTheme

Bases: Theme

Defines a Python related theme including the various font colours for syntax highlighting.

This is a light theme.

Source code in mu/interface/themes.py
class DayTheme(Theme):
    """
    Defines a Python related theme including the various font colours for
    syntax highlighting.

    This is a light theme.
    """

    FunctionMethodName = ClassName = Font(color="#0000a0")
    UnclosedString = Font(paper="#FFDDDD")
    Comment = CommentBlock = Font(color="gray")
    Keyword = Font(color="#005050", bold=True)
    SingleQuotedString = DoubleQuotedString = Font(color="#800000")
    SingleQuotedFString = DoubleQuotedFString = Font(color="#800000")
    TripleSingleQuotedString = TripleDoubleQuotedString = Font(color="#060")
    TripleSingleQuotedFString = TripleDoubleQuotedFString = Font(color="#060")
    Number = Font(color="#00008B")
    Decorator = Font(color="#cc6600")
    Default = Identifier = Font()
    Operator = Font(color="#400040")
    HighlightedIdentifier = Font(color="#0000a0")
    Paper = QColor("#FEFEF7")
    Caret = QColor("#181818")
    Margin = QColor("#EEE")
    IndicatorError = QColor("red")
    IndicatorStyle = QColor("blue")
    DebugStyle = QColor("#ffcc33")
    IndicatorWordMatch = QColor("lightGrey")
    BraceBackground = QColor("lightGrey")
    BraceForeground = QColor("blue")
    UnmatchedBraceBackground = QColor("#FFDDDD")
    UnmatchedBraceForeground = QColor("black")
    BreakpointMarker = QColor("#D80000")
    # HTML
    Tag = Keyword
    UnknownTag = Tag
    XMLTagEnd = Tag
    XMLStart = Tag
    XMLEnd = Tag
    Attribute = ClassName
    UnknownAttribute = Attribute
    HTMLNumber = Number
    HTMLDoubleQuotedString = DoubleQuotedString
    HTMLSingleQuotedString = SingleQuotedString
    OtherInTag = Default
    HTMLComment = Comment
    Entity = Operator
    CDATA = Decorator
    # CSS
    ClassSelector = Tag
    PseudoClass = ClassSelector
    UnknownPseudoClass = ClassSelector
    CSS1Property = CSS2Property = CSS3Property = UnknownProperty = SingleQuotedString
    Value = Number
    IDSelector = Tag
    Important = UnmatchedBraceBackground
    AtRule = Decorator
    MediaRule = Decorator
    Variable = HighlightedIdentifier

Font

Utility class that makes it easy to set font related values within the editor.

Source code in mu/interface/themes.py
class Font:
    """
    Utility class that makes it easy to set font related values within the
    editor.
    """

    _FONTS_LOADED = False

    def __init__(self, color="#181818", paper="#FEFEF7", bold=False, italic=False):
        self.color = color
        self.paper = paper
        self.bold = bold
        self.italic = italic

    @classmethod
    def get_database(cls):
        """
        Register Mu's builtin fonts with Qt's font database.

        Qt6's QFontDatabase is entirely static (it can no longer be
        instantiated), so this simply ensures the application fonts are loaded
        exactly once for the lifetime of the process.
        """
        if not cls._FONTS_LOADED:
            for variant in FONT_VARIANTS:
                filename = FONT_FILENAME_PATTERN.format(variant=variant)
                font_data = load_font_data(filename)
                QFontDatabase.addApplicationFontFromData(font_data)
            cls._FONTS_LOADED = True

    def load(self, size=DEFAULT_FONT_SIZE):
        """
        Load the font from the font database, using the correct size and style
        """
        Font.get_database()
        return QFontDatabase.font(FONT_NAME, self.stylename, size)

    @property
    def stylename(self):
        """
        Map the bold and italic boolean flags here to a relevant
        font style name.
        """
        if self.bold:
            if self.italic:
                return "Semibold Italic"
            return "Semibold"
        if self.italic:
            return "Italic"
        return "Regular"

stylename property

Map the bold and italic boolean flags here to a relevant font style name.

get_database() classmethod

Register Mu's builtin fonts with Qt's font database.

Qt6's QFontDatabase is entirely static (it can no longer be instantiated), so this simply ensures the application fonts are loaded exactly once for the lifetime of the process.

Source code in mu/interface/themes.py
@classmethod
def get_database(cls):
    """
    Register Mu's builtin fonts with Qt's font database.

    Qt6's QFontDatabase is entirely static (it can no longer be
    instantiated), so this simply ensures the application fonts are loaded
    exactly once for the lifetime of the process.
    """
    if not cls._FONTS_LOADED:
        for variant in FONT_VARIANTS:
            filename = FONT_FILENAME_PATTERN.format(variant=variant)
            font_data = load_font_data(filename)
            QFontDatabase.addApplicationFontFromData(font_data)
        cls._FONTS_LOADED = True

load(size=DEFAULT_FONT_SIZE)

Load the font from the font database, using the correct size and style

Source code in mu/interface/themes.py
def load(self, size=DEFAULT_FONT_SIZE):
    """
    Load the font from the font database, using the correct size and style
    """
    Font.get_database()
    return QFontDatabase.font(FONT_NAME, self.stylename, size)

NightTheme

Bases: Theme

Defines a Python related theme including the various font colours for syntax highlighting.

This is the dark theme.

Source code in mu/interface/themes.py
class NightTheme(Theme):
    """
    Defines a Python related theme including the various font colours for
    syntax highlighting.

    This is the dark theme.
    """

    # Python / General
    FunctionMethodName = ClassName = Font(color="#81a2be", paper="#222")
    UnclosedString = Font(paper="#c93827")
    Comment = CommentBlock = CommentLine = Font(color="#969896", paper="#222")
    Keyword = Font(color="#73a46a", bold=True, paper="#222")
    SingleQuotedString = DoubleQuotedString = Font(color="#f0c674", paper="#222")
    SingleQuotedFString = DoubleQuotedFString = Font(color="#f0c674", paper="#222")
    TripleSingleQuotedString = TripleDoubleQuotedString = Font(
        color="#f0c674", paper="#222"
    )
    TripleSingleQuotedFString = TripleDoubleQuotedFString = Font(
        color="#f0c674", paper="#222"
    )
    Number = Font(color="#b5bd68", paper="#222")
    Decorator = Font(color="#cc6666", paper="#222")
    Default = Identifier = Font(color="#DDD", paper="#222")
    Operator = Font(color="#b294bb", paper="#222")
    HighlightedIdentifier = Font(color="#de935f", paper="#222")
    Paper = QColor("#222")
    Caret = QColor("#c6c6c6")
    Margin = QColor("#424446")
    IndicatorError = QColor("#c93827")
    IndicatorStyle = QColor("#2f5692")
    DebugStyle = QColor("#444")
    IndicatorWordMatch = QColor("#f14721")
    BraceBackground = QColor("#ed1596")
    BraceForeground = QColor("#222")
    UnmatchedBraceBackground = QColor("#c93827")
    UnmatchedBraceForeground = QColor("#222")
    BreakpointMarker = QColor("#c93827")
    # HTML
    Tag = Keyword
    UnknownTag = Tag
    XMLTagEnd = Tag
    XMLStart = Tag
    XMLEnd = Tag
    Attribute = ClassName
    UnknownAttribute = Attribute
    HTMLNumber = Number
    HTMLDoubleQuotedString = DoubleQuotedString
    HTMLSingleQuotedString = SingleQuotedString
    OtherInTag = Default
    HTMLComment = Comment
    Entity = Operator
    CDATA = Decorator
    # CSS
    ClassSelector = Tag
    PseudoClass = ClassSelector
    UnknownPseudoClass = ClassSelector
    CSS1Property = CSS2Property = CSS3Property = UnknownProperty = SingleQuotedString
    Value = Number
    IDSelector = Tag
    Important = UnmatchedBraceBackground
    AtRule = Decorator
    MediaRule = Decorator
    Variable = HighlightedIdentifier

Theme

Defines a font and other theme specific related information.

Source code in mu/interface/themes.py
class Theme:
    """
    Defines a font and other theme specific related information.
    """

    @classmethod
    def apply_to(cls, lexer):
        # Apply a font for all styles
        lexer.setFont(Font().load())

        for name, font in cls.__dict__.items():
            if not isinstance(font, Font):
                continue
            if hasattr(lexer, name):
                style_num = getattr(lexer, name)
                lexer.setColor(QColor(font.color), style_num)
                lexer.setEolFill(True, style_num)
                lexer.setPaper(QColor(font.paper), style_num)
                lexer.setFont(font.load(), style_num)

mu.modes

Contains the definitions of the various modes Mu into which Mu can be put. All the core functionality is in the mu.modes.base module.

mu.modes.base

Core functionality and base classes for all Mu's modes. The definitions of API autocomplete and call tips can be found in the mu.modes.api namespace.

mu.modes.base

Contains the base classes for Mu editor modes.

Copyright (c) 2015-2017 Nicholas H.Tollervey and others (see the AUTHORS file).

This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/.

BaseMode

Bases: QObject

Represents the common aspects of a mode.

Source code in mu/modes/base.py
class BaseMode(QObject):
    """
    Represents the common aspects of a mode.
    """

    name = "UNNAMED MODE"
    short_name = "UNDEFINED_MODE"
    description = "DESCRIPTION NOT AVAILABLE."
    icon = "help"
    repl = False
    plotter = False
    is_debugger = False
    has_debugger = False
    save_timeout = 5  #: Number of seconds to wait before saving work.
    builtins = None  #: Symbols to assume as builtins when checking code style.
    file_extensions = []
    module_names = MODULE_NAMES
    code_template = _("# Write your code here :-)")
    #: The mu.plugin.Host that registered this mode (set by Host.register_mode).
    host = None

    def __init__(self, editor, view):
        self.editor = editor
        self.view = view
        super().__init__()

    def stop(self):
        """
        Called if/when the editor quits when in this mode. Override in child
        classes to clean up state, stop child processes etc.
        """
        pass  # Default is to do nothing

    def actions(self):
        """
        Return an ordered list of actions provided by this module. An action
        is a name (also used to identify the icon) , description, and handler.
        """
        return NotImplemented

    @staticmethod
    def workspace_dir():
        """
        Return the location on the filesystem for opening and closing files.

        The default is to use a directory in the users home folder, however
        in some network systems this in inaccessible. This allows a key in the
        settings file to be used to set a custom path.
        """
        workspace_dir = os.path.join(config.HOME_DIRECTORY, config.WORKSPACE_NAME)
        settings_workspace = settings.settings.get("workspace")

        if settings_workspace:
            if os.path.isdir(settings_workspace):
                logger.info(
                    "Using workspace {} from settings file".format(settings_workspace)
                )
                workspace_dir = settings_workspace
            else:
                logger.warning(
                    "Workspace {} in the settings file is not a valid "
                    "directory; using default {}".format(
                        settings_workspace, workspace_dir
                    )
                )

        return workspace_dir

    def assets_dir(self, asset_type):
        """
        Determine (and create) the directory for a set of assets

        This supports the [Images] and [Sounds] &c. buttons in pygamezero
        mode and possibly other modes, too.

        If a tab is current and has an active file, the assets directory
        is looked for under that path; otherwise the workspace directory
        is used.

        If the assets directory does not exist it is created
        """
        if self.view.current_tab and self.view.current_tab.path:
            base_dir = os.path.dirname(self.view.current_tab.path)
        else:
            base_dir = self.workspace_dir()
        assets_dir = os.path.join(base_dir, asset_type)
        os.makedirs(assets_dir, exist_ok=True)
        return assets_dir

    def api(self):
        """
        Return a list of API specifications to be used by auto-suggest and call
        tips.
        """
        return NotImplemented

    def set_buttons(self, **kwargs):
        """
        Given the names and boolean settings of buttons associated with actions
        for the current mode, toggles them into the boolean enabled state.
        """
        for k, v in kwargs.items():
            if k in self.view.button_bar.slots:
                self.view.button_bar.slots[k].setEnabled(bool(v))

    def return_focus_to_current_tab(self):
        """
        After, eg, stopping the plotter or closing the REPL return the focus
        to the currently-active tab is there is one.
        """
        if self.view.current_tab:
            self.view.current_tab.setFocus()

    def add_plotter(self):
        """
        Mode specific implementation of adding and connecting a plotter to
        incoming streams of data tuples.
        """
        return NotImplemented

    def write_plotter_data_to_csv(self, csv_filepath):
        """Write any plotter data out to a CSV file when the
        plotter is closed
        """
        with open(csv_filepath, "w", newline="") as csvfile:
            csv_writer = csv.writer(csvfile)
            csv_writer.writerows(self.view.plotter_pane.raw_data)

    def remove_plotter(self):
        """
        If there's an active plotter, hide it.

        Save any data captured while the plotter was active into a directory
        called 'data_capture' in the workspace directory. The file contains
        CSV data and is named with a timestamp for easy identification.
        """
        # Save the raw data as CSV
        data_dir = os.path.join(self.workspace_dir(), "data_capture")
        if not os.path.exists(data_dir):
            logger.debug("Creating directory: {}".format(data_dir))
            os.makedirs(data_dir)
        filename = "{}.csv".format(time.strftime("%Y%m%d-%H%M%S"))
        filepath = os.path.join(data_dir, filename)
        self.write_plotter_data_to_csv(filepath)
        self.view.remove_plotter()
        self.plotter = False
        logger.info("Removing plotter")
        self.return_focus_to_current_tab()

    def on_data_flood(self):
        """
        Handle when the plotter is being flooded by data (which usually causes
        Mu to become unresponsive). In this case, remove the plotter and
        display a warning dialog to explain what's happened and how to fix
        things (usually, put a time.sleep(x) into the code generating the
        data).
        """
        logger.error("Plotting data flood detected.")
        self.view.remove_plotter()
        self.plotter = False
        msg = _("Data Flood Detected!")
        info = _(
            "The plotter is flooded with data which will make Nu "
            "unresponsive and freeze. As a safeguard, the plotter has "
            "been stopped.\n\n"
            "Flooding is when chunks of data of more than 1024 bytes are "
            "repeatedly sent to the plotter.\n\n"
            "To fix this, make sure your code prints small tuples of "
            "data between calls to 'sleep' for a very short period of "
            "time."
        )
        self.view.show_message(msg, info)

    def open_file(self, path):
        """
        Some files are not plain text and each mode can attempt to decode them.

        When overridden, should return the text and newline convention for the
        file.
        """
        return None, None

    def activate(self):
        """
        Executed when the mode is activated
        """
        pass  # Default is to do nothing

    def ensure_state(self):
        """
        Executed when the mode is finished setting up. Used to ensure button /
        UI state according to current state of settings.
        """
        pass  # Default is to do nothing

    def deactivate(self):
        """
        Executed when the mode is activated
        """
        pass  # Default is to do nothing

    def device_changed(self, new_device):
        """
        Invoked when the user changes device.
        """
        pass

actions()

Return an ordered list of actions provided by this module. An action is a name (also used to identify the icon) , description, and handler.

Source code in mu/modes/base.py
def actions(self):
    """
    Return an ordered list of actions provided by this module. An action
    is a name (also used to identify the icon) , description, and handler.
    """
    return NotImplemented

activate()

Executed when the mode is activated

Source code in mu/modes/base.py
def activate(self):
    """
    Executed when the mode is activated
    """
    pass  # Default is to do nothing

add_plotter()

Mode specific implementation of adding and connecting a plotter to incoming streams of data tuples.

Source code in mu/modes/base.py
def add_plotter(self):
    """
    Mode specific implementation of adding and connecting a plotter to
    incoming streams of data tuples.
    """
    return NotImplemented

api()

Return a list of API specifications to be used by auto-suggest and call tips.

Source code in mu/modes/base.py
def api(self):
    """
    Return a list of API specifications to be used by auto-suggest and call
    tips.
    """
    return NotImplemented

assets_dir(asset_type)

Determine (and create) the directory for a set of assets

This supports the [Images] and [Sounds] &c. buttons in pygamezero mode and possibly other modes, too.

If a tab is current and has an active file, the assets directory is looked for under that path; otherwise the workspace directory is used.

If the assets directory does not exist it is created

Source code in mu/modes/base.py
def assets_dir(self, asset_type):
    """
    Determine (and create) the directory for a set of assets

    This supports the [Images] and [Sounds] &c. buttons in pygamezero
    mode and possibly other modes, too.

    If a tab is current and has an active file, the assets directory
    is looked for under that path; otherwise the workspace directory
    is used.

    If the assets directory does not exist it is created
    """
    if self.view.current_tab and self.view.current_tab.path:
        base_dir = os.path.dirname(self.view.current_tab.path)
    else:
        base_dir = self.workspace_dir()
    assets_dir = os.path.join(base_dir, asset_type)
    os.makedirs(assets_dir, exist_ok=True)
    return assets_dir

deactivate()

Executed when the mode is activated

Source code in mu/modes/base.py
def deactivate(self):
    """
    Executed when the mode is activated
    """
    pass  # Default is to do nothing

device_changed(new_device)

Invoked when the user changes device.

Source code in mu/modes/base.py
def device_changed(self, new_device):
    """
    Invoked when the user changes device.
    """
    pass

ensure_state()

Executed when the mode is finished setting up. Used to ensure button / UI state according to current state of settings.

Source code in mu/modes/base.py
def ensure_state(self):
    """
    Executed when the mode is finished setting up. Used to ensure button /
    UI state according to current state of settings.
    """
    pass  # Default is to do nothing

on_data_flood()

Handle when the plotter is being flooded by data (which usually causes Mu to become unresponsive). In this case, remove the plotter and display a warning dialog to explain what's happened and how to fix things (usually, put a time.sleep(x) into the code generating the data).

Source code in mu/modes/base.py
def on_data_flood(self):
    """
    Handle when the plotter is being flooded by data (which usually causes
    Mu to become unresponsive). In this case, remove the plotter and
    display a warning dialog to explain what's happened and how to fix
    things (usually, put a time.sleep(x) into the code generating the
    data).
    """
    logger.error("Plotting data flood detected.")
    self.view.remove_plotter()
    self.plotter = False
    msg = _("Data Flood Detected!")
    info = _(
        "The plotter is flooded with data which will make Nu "
        "unresponsive and freeze. As a safeguard, the plotter has "
        "been stopped.\n\n"
        "Flooding is when chunks of data of more than 1024 bytes are "
        "repeatedly sent to the plotter.\n\n"
        "To fix this, make sure your code prints small tuples of "
        "data between calls to 'sleep' for a very short period of "
        "time."
    )
    self.view.show_message(msg, info)

open_file(path)

Some files are not plain text and each mode can attempt to decode them.

When overridden, should return the text and newline convention for the file.

Source code in mu/modes/base.py
def open_file(self, path):
    """
    Some files are not plain text and each mode can attempt to decode them.

    When overridden, should return the text and newline convention for the
    file.
    """
    return None, None

remove_plotter()

If there's an active plotter, hide it.

Save any data captured while the plotter was active into a directory called 'data_capture' in the workspace directory. The file contains CSV data and is named with a timestamp for easy identification.

Source code in mu/modes/base.py
def remove_plotter(self):
    """
    If there's an active plotter, hide it.

    Save any data captured while the plotter was active into a directory
    called 'data_capture' in the workspace directory. The file contains
    CSV data and is named with a timestamp for easy identification.
    """
    # Save the raw data as CSV
    data_dir = os.path.join(self.workspace_dir(), "data_capture")
    if not os.path.exists(data_dir):
        logger.debug("Creating directory: {}".format(data_dir))
        os.makedirs(data_dir)
    filename = "{}.csv".format(time.strftime("%Y%m%d-%H%M%S"))
    filepath = os.path.join(data_dir, filename)
    self.write_plotter_data_to_csv(filepath)
    self.view.remove_plotter()
    self.plotter = False
    logger.info("Removing plotter")
    self.return_focus_to_current_tab()

return_focus_to_current_tab()

After, eg, stopping the plotter or closing the REPL return the focus to the currently-active tab is there is one.

Source code in mu/modes/base.py
def return_focus_to_current_tab(self):
    """
    After, eg, stopping the plotter or closing the REPL return the focus
    to the currently-active tab is there is one.
    """
    if self.view.current_tab:
        self.view.current_tab.setFocus()

set_buttons(**kwargs)

Given the names and boolean settings of buttons associated with actions for the current mode, toggles them into the boolean enabled state.

Source code in mu/modes/base.py
def set_buttons(self, **kwargs):
    """
    Given the names and boolean settings of buttons associated with actions
    for the current mode, toggles them into the boolean enabled state.
    """
    for k, v in kwargs.items():
        if k in self.view.button_bar.slots:
            self.view.button_bar.slots[k].setEnabled(bool(v))

stop()

Called if/when the editor quits when in this mode. Override in child classes to clean up state, stop child processes etc.

Source code in mu/modes/base.py
def stop(self):
    """
    Called if/when the editor quits when in this mode. Override in child
    classes to clean up state, stop child processes etc.
    """
    pass  # Default is to do nothing

workspace_dir() staticmethod

Return the location on the filesystem for opening and closing files.

The default is to use a directory in the users home folder, however in some network systems this in inaccessible. This allows a key in the settings file to be used to set a custom path.

Source code in mu/modes/base.py
@staticmethod
def workspace_dir():
    """
    Return the location on the filesystem for opening and closing files.

    The default is to use a directory in the users home folder, however
    in some network systems this in inaccessible. This allows a key in the
    settings file to be used to set a custom path.
    """
    workspace_dir = os.path.join(config.HOME_DIRECTORY, config.WORKSPACE_NAME)
    settings_workspace = settings.settings.get("workspace")

    if settings_workspace:
        if os.path.isdir(settings_workspace):
            logger.info(
                "Using workspace {} from settings file".format(settings_workspace)
            )
            workspace_dir = settings_workspace
        else:
            logger.warning(
                "Workspace {} in the settings file is not a valid "
                "directory; using default {}".format(
                    settings_workspace, workspace_dir
                )
            )

    return workspace_dir

write_plotter_data_to_csv(csv_filepath)

Write any plotter data out to a CSV file when the plotter is closed

Source code in mu/modes/base.py
def write_plotter_data_to_csv(self, csv_filepath):
    """Write any plotter data out to a CSV file when the
    plotter is closed
    """
    with open(csv_filepath, "w", newline="") as csvfile:
        csv_writer = csv.writer(csvfile)
        csv_writer.writerows(self.view.plotter_pane.raw_data)

MicroPythonMode

Bases: BaseMode

Includes functionality that works with a USB serial based REPL.

Source code in mu/modes/base.py
class MicroPythonMode(BaseMode):
    """
    Includes functionality that works with a USB serial based REPL.
    """

    valid_boards = []
    force_interrupt = True
    connection = None
    baudrate = 115200
    builtins = ["const"]

    def compatible_board(self, port):
        """
        A compatible board must match on vendor ID, but only needs to
        match on product ID or manufacturer ID, if they are supplied
        in the list of valid boards (aren't None).
        """
        pid = port.productIdentifier()
        vid = port.vendorIdentifier()
        manufacturer = port.manufacturer()
        serial_number = port.serialNumber()
        port_name = self.port_path(port.portName())

        for v, p, m, device_name in self.valid_boards:
            if (
                v == vid
                and (p == pid or p is None)
                and (m == manufacturer or m is None)
            ):
                return Device(
                    vid,
                    pid,
                    port_name,
                    serial_number,
                    manufacturer,
                    self.name,
                    self.short_name,
                    device_name,
                )
        return None

    def find_devices(self, with_logging=True):
        """
        Returns the port and serial number, and name for the first
        MicroPython-ish device found connected to the host computer.
        If no device is found, returns the tuple (None, None, None).
        """
        available_ports = QSerialPortInfo.availablePorts()
        devices = []
        for port in available_ports:
            device = self.compatible_board(port)
            if device:
                # On OS X devices show up with two different port
                # numbers (/dev/tty.usbserial-xxx and
                # /dev/cu.usbserial-xxx) we only want to display the
                # ones on ports names /dev/cu.usbserial-xxx to users
                if sys.platform == "darwin" and device.port[:7] != "/dev/cu":
                    continue
                if with_logging:
                    logger.info("Found device on port: {}".format(device.port))
                    logger.info("Serial number: {}".format(device.serial_number))
                    if device.board_name:
                        logger.info("Board type: {}".format(device.board_name))
                devices.append(device)
        if not devices and with_logging:
            logger.warning("Could not find device.")
            logger.debug("Available ports:")
            logger.debug(
                [
                    "PID:0x{:04x} VID:0x{:04x} PORT:{}".format(
                        p.productIdentifier(),
                        p.vendorIdentifier(),
                        p.portName(),
                    )
                    for p in available_ports
                ]
            )
        return devices

    def port_path(self, port_name):
        if os.name == "posix":
            # If we're on Linux or OSX reference the port is like this...
            return "/dev/{}".format(port_name)
        elif os.name == "nt":
            # On Windows simply return the port (e.g. COM0).
            return port_name
        else:
            # No idea how to deal with other OS's so fail.
            raise NotImplementedError('OS "{}" not supported.'.format(os.name))

    def toggle_repl(self, event):
        """
        Toggles the REPL on and off.
        """
        if self.repl:
            self.remove_repl()
            logger.info("Toggle REPL off.")
        else:
            self.add_repl()
            logger.info("Toggle REPL on.")

    def remove_repl(self):
        """
        If there's an active REPL, disconnect and hide it.
        """
        if not self.plotter and self.connection:
            self.connection.close()
            self.connection = None
        self.view.remove_repl()
        self.repl = False

    def add_repl(self):
        """
        Detect a connected MicroPython based device and, if found, connect to
        the REPL and display it to the user.
        """
        device = self.editor.current_device
        if device:
            try:
                if not self.connection:
                    self.connection = REPLConnection(device.port, self.baudrate)
                    self.connection.open()
                    if self.force_interrupt:
                        self.connection.send_interrupt()

                self.view.add_micropython_repl(self.name, self.connection)
                logger.info("Started REPL on port: {}".format(device.port))
                self.repl = True
            except IOError as ex:
                logger.error(ex)
                self.repl = False
                info = _(
                    "Click on the device's reset button, wait a few"
                    " seconds and then try again."
                )
                self.view.show_message(str(ex), info)
            except Exception as ex:
                logger.error(ex)
        else:
            message = _("Could not find an attached device.")
            information = _(
                "Please make sure the device is plugged into this"
                " computer.\n\nIt must have a version of"
                " MicroPython (or CircuitPython) flashed onto it"
                " before the REPL will work.\n\nFinally, press the"
                " device's reset button and wait a few seconds"
                " before trying again."
            )
            self.view.show_message(message, information)

    def toggle_plotter(self, event):
        """
        Toggles the plotter on and off.
        """
        if self.plotter:
            self.remove_plotter()
            logger.info("Toggle plotter off.")
        else:
            self.add_plotter()
            logger.info("Toggle plotter on.")

    def add_plotter(self):
        """
        Check if REPL exists, and if so, enable the plotter pane!
        """
        device = self.editor.current_device
        if device:
            try:
                if not self.connection:
                    self.connection = REPLConnection(device.port, self.baudrate)
                    self.connection.open()
                self.view.add_micropython_plotter(
                    self.name, self.connection, self.on_data_flood
                )
                logger.info("Started plotter")
                self.plotter = True
            except IOError as ex:
                logger.error(ex)
                self.plotter = False
                info = _(
                    "Click on the device's reset button, wait a few"
                    " seconds and then try again."
                )
                self.view.show_message(str(ex), info)
            except Exception as ex:
                logger.error(ex)
        else:
            message = _("Could not find an attached device.")
            information = _(
                "Please make sure the device is plugged into this"
                " computer.\n\nIt must have a version of"
                " MicroPython (or CircuitPython) flashed onto it"
                " before the Plotter will work.\n\nFinally, press"
                " the device's reset button and wait a few seconds"
                " before trying again."
            )
            self.view.show_message(message, information)

    def on_data_flood(self):
        """
        Ensure the REPL is stopped if there is data flooding of the plotter.
        """
        self.remove_repl()
        super().on_data_flood()

    def remove_plotter(self):
        """
        Remove plotter pane. Disconnects serial connection to device.
        """
        if not self.repl and self.connection:
            self.connection.close()
            self.connection = None
        super().remove_plotter()

    def activate(self):
        """
        Invoked whenever the mode is activated.
        """
        self.view.show_device_selector()

    def deactivate(self):
        """
        Invoked whenever the mode is deactivated.
        """
        # Remove REPL/Plotter if they are active
        self.view.hide_device_selector()
        if self.plotter:
            self.remove_plotter()
        if self.repl:
            self.remove_repl()

    def device_changed(self, new_device):
        """
        Invoked when the user changes device.
        """
        # Reconnect REPL, Plotter and send interrupt
        if self.repl:
            self.remove_repl()
            self.add_repl()
        if self.plotter:
            self.remove_plotter()
            self.add_plotter()
        if self.connection:
            self.connection.send_interrupt()

activate()

Invoked whenever the mode is activated.

Source code in mu/modes/base.py
def activate(self):
    """
    Invoked whenever the mode is activated.
    """
    self.view.show_device_selector()

add_plotter()

Check if REPL exists, and if so, enable the plotter pane!

Source code in mu/modes/base.py
def add_plotter(self):
    """
    Check if REPL exists, and if so, enable the plotter pane!
    """
    device = self.editor.current_device
    if device:
        try:
            if not self.connection:
                self.connection = REPLConnection(device.port, self.baudrate)
                self.connection.open()
            self.view.add_micropython_plotter(
                self.name, self.connection, self.on_data_flood
            )
            logger.info("Started plotter")
            self.plotter = True
        except IOError as ex:
            logger.error(ex)
            self.plotter = False
            info = _(
                "Click on the device's reset button, wait a few"
                " seconds and then try again."
            )
            self.view.show_message(str(ex), info)
        except Exception as ex:
            logger.error(ex)
    else:
        message = _("Could not find an attached device.")
        information = _(
            "Please make sure the device is plugged into this"
            " computer.\n\nIt must have a version of"
            " MicroPython (or CircuitPython) flashed onto it"
            " before the Plotter will work.\n\nFinally, press"
            " the device's reset button and wait a few seconds"
            " before trying again."
        )
        self.view.show_message(message, information)

add_repl()

Detect a connected MicroPython based device and, if found, connect to the REPL and display it to the user.

Source code in mu/modes/base.py
def add_repl(self):
    """
    Detect a connected MicroPython based device and, if found, connect to
    the REPL and display it to the user.
    """
    device = self.editor.current_device
    if device:
        try:
            if not self.connection:
                self.connection = REPLConnection(device.port, self.baudrate)
                self.connection.open()
                if self.force_interrupt:
                    self.connection.send_interrupt()

            self.view.add_micropython_repl(self.name, self.connection)
            logger.info("Started REPL on port: {}".format(device.port))
            self.repl = True
        except IOError as ex:
            logger.error(ex)
            self.repl = False
            info = _(
                "Click on the device's reset button, wait a few"
                " seconds and then try again."
            )
            self.view.show_message(str(ex), info)
        except Exception as ex:
            logger.error(ex)
    else:
        message = _("Could not find an attached device.")
        information = _(
            "Please make sure the device is plugged into this"
            " computer.\n\nIt must have a version of"
            " MicroPython (or CircuitPython) flashed onto it"
            " before the REPL will work.\n\nFinally, press the"
            " device's reset button and wait a few seconds"
            " before trying again."
        )
        self.view.show_message(message, information)

compatible_board(port)

A compatible board must match on vendor ID, but only needs to match on product ID or manufacturer ID, if they are supplied in the list of valid boards (aren't None).

Source code in mu/modes/base.py
def compatible_board(self, port):
    """
    A compatible board must match on vendor ID, but only needs to
    match on product ID or manufacturer ID, if they are supplied
    in the list of valid boards (aren't None).
    """
    pid = port.productIdentifier()
    vid = port.vendorIdentifier()
    manufacturer = port.manufacturer()
    serial_number = port.serialNumber()
    port_name = self.port_path(port.portName())

    for v, p, m, device_name in self.valid_boards:
        if (
            v == vid
            and (p == pid or p is None)
            and (m == manufacturer or m is None)
        ):
            return Device(
                vid,
                pid,
                port_name,
                serial_number,
                manufacturer,
                self.name,
                self.short_name,
                device_name,
            )
    return None

deactivate()

Invoked whenever the mode is deactivated.

Source code in mu/modes/base.py
def deactivate(self):
    """
    Invoked whenever the mode is deactivated.
    """
    # Remove REPL/Plotter if they are active
    self.view.hide_device_selector()
    if self.plotter:
        self.remove_plotter()
    if self.repl:
        self.remove_repl()

device_changed(new_device)

Invoked when the user changes device.

Source code in mu/modes/base.py
def device_changed(self, new_device):
    """
    Invoked when the user changes device.
    """
    # Reconnect REPL, Plotter and send interrupt
    if self.repl:
        self.remove_repl()
        self.add_repl()
    if self.plotter:
        self.remove_plotter()
        self.add_plotter()
    if self.connection:
        self.connection.send_interrupt()

find_devices(with_logging=True)

Returns the port and serial number, and name for the first MicroPython-ish device found connected to the host computer. If no device is found, returns the tuple (None, None, None).

Source code in mu/modes/base.py
def find_devices(self, with_logging=True):
    """
    Returns the port and serial number, and name for the first
    MicroPython-ish device found connected to the host computer.
    If no device is found, returns the tuple (None, None, None).
    """
    available_ports = QSerialPortInfo.availablePorts()
    devices = []
    for port in available_ports:
        device = self.compatible_board(port)
        if device:
            # On OS X devices show up with two different port
            # numbers (/dev/tty.usbserial-xxx and
            # /dev/cu.usbserial-xxx) we only want to display the
            # ones on ports names /dev/cu.usbserial-xxx to users
            if sys.platform == "darwin" and device.port[:7] != "/dev/cu":
                continue
            if with_logging:
                logger.info("Found device on port: {}".format(device.port))
                logger.info("Serial number: {}".format(device.serial_number))
                if device.board_name:
                    logger.info("Board type: {}".format(device.board_name))
            devices.append(device)
    if not devices and with_logging:
        logger.warning("Could not find device.")
        logger.debug("Available ports:")
        logger.debug(
            [
                "PID:0x{:04x} VID:0x{:04x} PORT:{}".format(
                    p.productIdentifier(),
                    p.vendorIdentifier(),
                    p.portName(),
                )
                for p in available_ports
            ]
        )
    return devices

on_data_flood()

Ensure the REPL is stopped if there is data flooding of the plotter.

Source code in mu/modes/base.py
def on_data_flood(self):
    """
    Ensure the REPL is stopped if there is data flooding of the plotter.
    """
    self.remove_repl()
    super().on_data_flood()

remove_plotter()

Remove plotter pane. Disconnects serial connection to device.

Source code in mu/modes/base.py
def remove_plotter(self):
    """
    Remove plotter pane. Disconnects serial connection to device.
    """
    if not self.repl and self.connection:
        self.connection.close()
        self.connection = None
    super().remove_plotter()

remove_repl()

If there's an active REPL, disconnect and hide it.

Source code in mu/modes/base.py
def remove_repl(self):
    """
    If there's an active REPL, disconnect and hide it.
    """
    if not self.plotter and self.connection:
        self.connection.close()
        self.connection = None
    self.view.remove_repl()
    self.repl = False

toggle_plotter(event)

Toggles the plotter on and off.

Source code in mu/modes/base.py
def toggle_plotter(self, event):
    """
    Toggles the plotter on and off.
    """
    if self.plotter:
        self.remove_plotter()
        logger.info("Toggle plotter off.")
    else:
        self.add_plotter()
        logger.info("Toggle plotter on.")

toggle_repl(event)

Toggles the REPL on and off.

Source code in mu/modes/base.py
def toggle_repl(self, event):
    """
    Toggles the REPL on and off.
    """
    if self.repl:
        self.remove_repl()
        logger.info("Toggle REPL off.")
    else:
        self.add_repl()
        logger.info("Toggle REPL on.")

REPLConnection

Bases: QObject

Source code in mu/modes/base.py
class REPLConnection(QObject):
    serial = None
    data_received = pyqtSignal(bytes)
    connection_error = pyqtSignal(str)

    def __init__(self, port, baudrate=115200):
        super().__init__()
        self.serial = QSerialPort()
        self._port = port
        self._baudrate = baudrate
        self.serial.setPortName(port)
        self.serial.setBaudRate(baudrate)

    @property
    def port(self):
        if self.serial:
            # perhaps return self.serial.portName()?
            return self._port
        else:
            return None

    @property
    def baudrate(self):
        if self.serial:
            # perhaps return self.serial.baudRate()
            return self._baudrate
        else:
            return None

    def open(self):
        """
        Open the serial link
        """

        logger.info("Connecting to REPL on port: {}".format(self.port))

        if not self.serial.open(QIODevice.OpenModeFlag.ReadWrite):
            msg = _("Cannot connect to device on port {}").format(self.port)
            raise IOError(msg)

        self.serial.setDataTerminalReady(True)
        if not self.serial.isDataTerminalReady():
            # Using pyserial as a 'hack' to open the port and set DTR
            # as QtSerial does not seem to work on some Windows :(
            # See issues #281 and #302 for details.
            self.serial.close()
            pyser = Serial(self.port)  # open serial port w/pyserial
            pyser.dtr = True
            pyser.close()
            self.serial.open(QIODevice.OpenModeFlag.ReadWrite)
        self.serial.readyRead.connect(self._on_serial_read)

        logger.info("Connected to REPL on port: {}".format(self.port))

    def close(self):
        """
        Close and clean up the currently open serial link.
        """
        logger.info("Closing connection to REPL on port: {}".format(self.port))
        if self.serial:
            self.serial.close()
            self.serial = None

    def _on_serial_read(self):
        """
        Called when data is ready to be send from the device
        """
        data = bytes(self.serial.readAll())
        self.data_received.emit(data)

    def write(self, data):
        self.serial.write(data)

    def send_interrupt(self):
        self.write(EXIT_RAW_MODE)  # CTRL-B
        self.write(KEYBOARD_INTERRUPT)  # CTRL-C

    def execute(self, commands):
        """
        Execute a series of commands over a period of time (scheduling
        remaining commands to be run in the next iteration of the event loop).
        """
        if commands:
            command = commands[0]
            logger.info("Sending command {}".format(command))
            self.write(command)
            remainder = commands[1:]
            remaining_task = lambda commands=remainder: self.execute(commands)
            QTimer.singleShot(2, remaining_task)

    def send_commands(self, commands):
        """
        Send commands to the REPL via raw mode.
        """
        # Sequence of commands to get into raw mode (From pyboard.py).
        raw_on = [
            KEYBOARD_INTERRUPT,
            KEYBOARD_INTERRUPT,
            ENTER_RAW_MODE,
            SOFT_REBOOT,
            KEYBOARD_INTERRUPT,
            KEYBOARD_INTERRUPT,
        ]

        newline = [b'print("\\n");']
        commands = [c.encode("utf-8") + b"\r" for c in commands]
        commands.append(b"\r")
        commands.append(SOFT_REBOOT)
        raw_off = [EXIT_RAW_MODE]
        command_sequence = raw_on + newline + commands + raw_off
        logger.info(command_sequence)
        self.execute(command_sequence)

close()

Close and clean up the currently open serial link.

Source code in mu/modes/base.py
def close(self):
    """
    Close and clean up the currently open serial link.
    """
    logger.info("Closing connection to REPL on port: {}".format(self.port))
    if self.serial:
        self.serial.close()
        self.serial = None

execute(commands)

Execute a series of commands over a period of time (scheduling remaining commands to be run in the next iteration of the event loop).

Source code in mu/modes/base.py
def execute(self, commands):
    """
    Execute a series of commands over a period of time (scheduling
    remaining commands to be run in the next iteration of the event loop).
    """
    if commands:
        command = commands[0]
        logger.info("Sending command {}".format(command))
        self.write(command)
        remainder = commands[1:]
        remaining_task = lambda commands=remainder: self.execute(commands)
        QTimer.singleShot(2, remaining_task)

open()

Open the serial link

Source code in mu/modes/base.py
def open(self):
    """
    Open the serial link
    """

    logger.info("Connecting to REPL on port: {}".format(self.port))

    if not self.serial.open(QIODevice.OpenModeFlag.ReadWrite):
        msg = _("Cannot connect to device on port {}").format(self.port)
        raise IOError(msg)

    self.serial.setDataTerminalReady(True)
    if not self.serial.isDataTerminalReady():
        # Using pyserial as a 'hack' to open the port and set DTR
        # as QtSerial does not seem to work on some Windows :(
        # See issues #281 and #302 for details.
        self.serial.close()
        pyser = Serial(self.port)  # open serial port w/pyserial
        pyser.dtr = True
        pyser.close()
        self.serial.open(QIODevice.OpenModeFlag.ReadWrite)
    self.serial.readyRead.connect(self._on_serial_read)

    logger.info("Connected to REPL on port: {}".format(self.port))

send_commands(commands)

Send commands to the REPL via raw mode.

Source code in mu/modes/base.py
def send_commands(self, commands):
    """
    Send commands to the REPL via raw mode.
    """
    # Sequence of commands to get into raw mode (From pyboard.py).
    raw_on = [
        KEYBOARD_INTERRUPT,
        KEYBOARD_INTERRUPT,
        ENTER_RAW_MODE,
        SOFT_REBOOT,
        KEYBOARD_INTERRUPT,
        KEYBOARD_INTERRUPT,
    ]

    newline = [b'print("\\n");']
    commands = [c.encode("utf-8") + b"\r" for c in commands]
    commands.append(b"\r")
    commands.append(SOFT_REBOOT)
    raw_off = [EXIT_RAW_MODE]
    command_sequence = raw_on + newline + commands + raw_off
    logger.info(command_sequence)
    self.execute(command_sequence)

mu.modes.circuitpython

CircuitPython mode for Adafruit boards (and others).

mu.modes.circuitpython

A mode for working with Circuit Python boards.

Copyright (c) 2015-2017 Nicholas H.Tollervey and others (see the AUTHORS file).

This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/.

CircuitPythonMode

Bases: MicroPythonMode

Represents the functionality required by the CircuitPython mode.

Source code in mu/modes/circuitpython.py
class CircuitPythonMode(MicroPythonMode):
    """
    Represents the functionality required by the CircuitPython mode.
    """

    name = _("CircuitPython")
    short_name = "circuitpython"
    description = _("Write code for boards running CircuitPython.")
    icon = "circuitpython"
    save_timeout = 0  #: No auto-save on CP boards. Will restart.
    connected = True  #: is the board connected.
    force_interrupt = False  #: NO keyboard interrupt on serial connection.

    # Modules built into CircuitPython which mustn't be used as file names
    # for source code.
    module_names = {
        "_bleio",
        "_eve",
        "_pew",
        "_pixelbuf",
        "_stage",
        "_typing",
        "adafruit_bus_device",
        "aesio",
        "alarm",
        "array",
        "analogio",
        "audiobusio",
        "audiocore",
        "audioio",
        "audiomixer",
        "audiomp3",
        "audiopwmio",
        "binascii",
        "bitbangio",
        "bitmaptools",
        "bitops",
        "board",
        "builtins",
        "busio",
        "camera",
        "canio",
        "collections",
        "countio",
        "digitalio",
        "displayio",
        "dualbank",
        "errno",
        "fontio",
        "framebufferio",
        "frequencyio",
        "gamepad",
        "gamepadshift",
        "gc",
        "gnss",
        "hashlib",
        "i2cperipheral",
        "io",
        "ipaddress",
        "json",
        "math",
        "memorymonitor",
        "microcontroller",
        "msgpack",
        "multiterminal",
        "neopixel_write",
        "network",
        "nvm",
        "os",
        "ps2io",
        "pulseio",
        "pwmio",
        "random",
        "re",
        "rgbmatrix",
        "rotaryio",
        "rtc",
        "sdcardio",
        "sdioio",
        "sharpdisplay",
        "socket",
        "socketpool",
        "ssl",
        "storage",
        "struct",
        "supervisor",
        "sys",
        "terminalio",
        "time",
        "touchio",
        "uheap",
        "usb_cdc",
        "usb_hid",
        "usb_midi",
        "ustack",
        "vectorio",
        "watchdog",
        "wifi",
        "wiznet",
        "zlib",
    }

    def actions(self):
        """
        Return an ordered list of actions provided by this module. An action
        is a name (also used to identify the icon) , description, and handler.
        """
        buttons = [
            {
                "name": "serial",
                "display_name": _("Serial"),
                "description": _("Open a serial connection to your device."),
                "handler": self.toggle_repl,
                "shortcut": "CTRL+Shift+U",
            }
        ]
        if CHARTS:
            buttons.append(
                {
                    "name": "plotter",
                    "display_name": _("Plotter"),
                    "description": _("Plot incoming REPL data."),
                    "handler": self.toggle_plotter,
                    "shortcut": "CTRL+Shift+P",
                }
            )
        return buttons

    def workspace_dir(self):
        """
        Return the default location on the filesystem for opening and closing
        files.
        """
        device_dir = None
        # Attempts to find the path on the filesystem that represents the
        # plugged in CIRCUITPY board.
        if os.name == "posix":
            # We're on Linux or OSX
            for mount_command in ["mount", "/sbin/mount"]:
                try:
                    mount_output = check_output(mount_command).splitlines()
                    mounted_volumes = [x.split()[2] for x in mount_output]
                    for volume in mounted_volumes:
                        tail = os.path.split(volume)[-1]
                        if tail.startswith(b"CIRCUITPY") or tail.startswith(
                            b"PYBFLASH"
                        ):
                            device_dir = volume.decode("utf-8")
                            break
                except FileNotFoundError:
                    pass
                except PermissionError as e:
                    logger.error(
                        "Received '{}' running command: {}".format(
                            repr(e), mount_command
                        )
                    )
                    m = _("Permission error running mount command")
                    info = _(
                        'The mount command ("{}") returned an error: '
                        "{}. Nu will continue as if a device isn't "
                        "plugged in."
                    ).format(mount_command, repr(e))
                    self.view.show_message(m, info)
                # Avoid crashing Mu, the workspace dir will be set to default
                except Exception as e:
                    logger.error(
                        "Received '{}' running command: {}".format(
                            repr(e), mount_command
                        )
                    )
            if os.path.exists("/mnt/chromeos"):
                # We're on ChromeOS
                if os.path.exists("/mnt/chromeos/removable/CIRCUITPY/"):
                    device_dir = "/mnt/chromeos/removable/CIRCUITPY/"
                else:
                    m = _(
                        "If your Circuit Python device is plugged in,"
                        + ' you need to "Share with Linux" on the CIRCUITPY drive'
                        + ' in the "Files" app then restart Nu.'
                    )
                    self.view.show_message(m)

        elif os.name == "nt":
            # We're on Windows.

            def get_volume_name(disk_name):
                """
                Each disk or external device connected to windows has an
                attribute called "volume name". This function returns the
                volume name for the given disk/device.

                Code from http://stackoverflow.com/a/12056414
                """
                vol_name_buf = ctypes.create_unicode_buffer(1024)
                ctypes.windll.kernel32.GetVolumeInformationW(
                    ctypes.c_wchar_p(disk_name),
                    vol_name_buf,
                    ctypes.sizeof(vol_name_buf),
                    None,
                    None,
                    None,
                    None,
                    0,
                )
                return vol_name_buf.value

            #
            # In certain circumstances, volumes are allocated to USB
            # storage devices which cause a Windows popup to raise if their
            # volume contains no media. Wrapping the check in SetErrorMode
            # with SEM_FAILCRITICALERRORS (1) prevents this popup.
            #
            old_mode = ctypes.windll.kernel32.SetErrorMode(1)
            try:
                for disk in "ABCDEFGHIJKLMNOPQRSTUVWXYZ":
                    path = "{}:\\".format(disk)
                    if os.path.exists(path) and get_volume_name(path) == "CIRCUITPY":
                        return path
            finally:
                ctypes.windll.kernel32.SetErrorMode(old_mode)
        else:
            # No support for unknown operating systems.
            raise NotImplementedError('OS "{}" not supported.'.format(os.name))

        if device_dir:
            # Found it!
            self.connected = True
            return device_dir
        else:
            # Not plugged in? Just return Mu's regular workspace directory
            # after warning the user.
            wd = super().workspace_dir()
            if self.connected:
                m = _("Could not find an attached CircuitPython device.")
                info = _(
                    "Python files for CircuitPython devices"
                    " are stored on the device. Therefore, to edit"
                    " these files you need to have the device plugged in."
                    " Until you plug in a device, Nu will use the"
                    " directory found here:\n\n"
                    " {}\n\n...to store your code."
                )
                self.view.show_message(m, info.format(wd))
                self.connected = False
            return wd

    def compatible_board(self, port):
        """Use adafruit_board_toolkit to find out whether a board is running
        CircuitPython. The toolkit sees if the CDC Interface name is appropriate.
        """

        pid = port.productIdentifier()
        vid = port.vendorIdentifier()
        manufacturer = port.manufacturer()
        serial_number = port.serialNumber()
        port_name = self.port_path(port.portName())

        # Find all the CircuitPython REPL comports,
        # and see if any of their device names match the one passed in.
        for comport in circuitpython_serial.repl_comports():
            if comport.device == port_name:
                return Device(
                    vid,
                    pid,
                    port_name,
                    serial_number,
                    manufacturer,
                    self.name,
                    self.short_name,
                    "CircuitPython board",
                )
        # No match.
        return None

    def api(self):
        """
        Return a list of API specifications to be used by auto-suggest and call
        tips.
        """
        return SHARED_APIS + ADAFRUIT_APIS

actions()

Return an ordered list of actions provided by this module. An action is a name (also used to identify the icon) , description, and handler.

Source code in mu/modes/circuitpython.py
def actions(self):
    """
    Return an ordered list of actions provided by this module. An action
    is a name (also used to identify the icon) , description, and handler.
    """
    buttons = [
        {
            "name": "serial",
            "display_name": _("Serial"),
            "description": _("Open a serial connection to your device."),
            "handler": self.toggle_repl,
            "shortcut": "CTRL+Shift+U",
        }
    ]
    if CHARTS:
        buttons.append(
            {
                "name": "plotter",
                "display_name": _("Plotter"),
                "description": _("Plot incoming REPL data."),
                "handler": self.toggle_plotter,
                "shortcut": "CTRL+Shift+P",
            }
        )
    return buttons

api()

Return a list of API specifications to be used by auto-suggest and call tips.

Source code in mu/modes/circuitpython.py
def api(self):
    """
    Return a list of API specifications to be used by auto-suggest and call
    tips.
    """
    return SHARED_APIS + ADAFRUIT_APIS

compatible_board(port)

Use adafruit_board_toolkit to find out whether a board is running CircuitPython. The toolkit sees if the CDC Interface name is appropriate.

Source code in mu/modes/circuitpython.py
def compatible_board(self, port):
    """Use adafruit_board_toolkit to find out whether a board is running
    CircuitPython. The toolkit sees if the CDC Interface name is appropriate.
    """

    pid = port.productIdentifier()
    vid = port.vendorIdentifier()
    manufacturer = port.manufacturer()
    serial_number = port.serialNumber()
    port_name = self.port_path(port.portName())

    # Find all the CircuitPython REPL comports,
    # and see if any of their device names match the one passed in.
    for comport in circuitpython_serial.repl_comports():
        if comport.device == port_name:
            return Device(
                vid,
                pid,
                port_name,
                serial_number,
                manufacturer,
                self.name,
                self.short_name,
                "CircuitPython board",
            )
    # No match.
    return None

workspace_dir()

Return the default location on the filesystem for opening and closing files.

Source code in mu/modes/circuitpython.py
def workspace_dir(self):
    """
    Return the default location on the filesystem for opening and closing
    files.
    """
    device_dir = None
    # Attempts to find the path on the filesystem that represents the
    # plugged in CIRCUITPY board.
    if os.name == "posix":
        # We're on Linux or OSX
        for mount_command in ["mount", "/sbin/mount"]:
            try:
                mount_output = check_output(mount_command).splitlines()
                mounted_volumes = [x.split()[2] for x in mount_output]
                for volume in mounted_volumes:
                    tail = os.path.split(volume)[-1]
                    if tail.startswith(b"CIRCUITPY") or tail.startswith(
                        b"PYBFLASH"
                    ):
                        device_dir = volume.decode("utf-8")
                        break
            except FileNotFoundError:
                pass
            except PermissionError as e:
                logger.error(
                    "Received '{}' running command: {}".format(
                        repr(e), mount_command
                    )
                )
                m = _("Permission error running mount command")
                info = _(
                    'The mount command ("{}") returned an error: '
                    "{}. Nu will continue as if a device isn't "
                    "plugged in."
                ).format(mount_command, repr(e))
                self.view.show_message(m, info)
            # Avoid crashing Mu, the workspace dir will be set to default
            except Exception as e:
                logger.error(
                    "Received '{}' running command: {}".format(
                        repr(e), mount_command
                    )
                )
        if os.path.exists("/mnt/chromeos"):
            # We're on ChromeOS
            if os.path.exists("/mnt/chromeos/removable/CIRCUITPY/"):
                device_dir = "/mnt/chromeos/removable/CIRCUITPY/"
            else:
                m = _(
                    "If your Circuit Python device is plugged in,"
                    + ' you need to "Share with Linux" on the CIRCUITPY drive'
                    + ' in the "Files" app then restart Nu.'
                )
                self.view.show_message(m)

    elif os.name == "nt":
        # We're on Windows.

        def get_volume_name(disk_name):
            """
            Each disk or external device connected to windows has an
            attribute called "volume name". This function returns the
            volume name for the given disk/device.

            Code from http://stackoverflow.com/a/12056414
            """
            vol_name_buf = ctypes.create_unicode_buffer(1024)
            ctypes.windll.kernel32.GetVolumeInformationW(
                ctypes.c_wchar_p(disk_name),
                vol_name_buf,
                ctypes.sizeof(vol_name_buf),
                None,
                None,
                None,
                None,
                0,
            )
            return vol_name_buf.value

        #
        # In certain circumstances, volumes are allocated to USB
        # storage devices which cause a Windows popup to raise if their
        # volume contains no media. Wrapping the check in SetErrorMode
        # with SEM_FAILCRITICALERRORS (1) prevents this popup.
        #
        old_mode = ctypes.windll.kernel32.SetErrorMode(1)
        try:
            for disk in "ABCDEFGHIJKLMNOPQRSTUVWXYZ":
                path = "{}:\\".format(disk)
                if os.path.exists(path) and get_volume_name(path) == "CIRCUITPY":
                    return path
        finally:
            ctypes.windll.kernel32.SetErrorMode(old_mode)
    else:
        # No support for unknown operating systems.
        raise NotImplementedError('OS "{}" not supported.'.format(os.name))

    if device_dir:
        # Found it!
        self.connected = True
        return device_dir
    else:
        # Not plugged in? Just return Mu's regular workspace directory
        # after warning the user.
        wd = super().workspace_dir()
        if self.connected:
            m = _("Could not find an attached CircuitPython device.")
            info = _(
                "Python files for CircuitPython devices"
                " are stored on the device. Therefore, to edit"
                " these files you need to have the device plugged in."
                " Until you plug in a device, Nu will use the"
                " directory found here:\n\n"
                " {}\n\n...to store your code."
            )
            self.view.show_message(m, info.format(wd))
            self.connected = False
        return wd

mu.modes.debugger

The Python 3 debugger mode.

mu.modes.debugger

The mode Mu is is when it's debugging a Python 3 script.

Copyright (c) 2015-2017 Nicholas H.Tollervey and others (see the AUTHORS file).

This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/.

DebugMode

Bases: BaseMode

Represents the functionality required by the Python 3 visual debugger.

Source code in mu/modes/debugger.py
class DebugMode(BaseMode):
    """
    Represents the functionality required by the Python 3 visual debugger.
    """

    name = _("Graphical Debugger")
    short_name = "debugger"
    description = _("Debug your Python 3 code.")
    icon = "python"
    runner = None
    is_debugger = True
    save_timeout = 0  # No need to auto-save when in read-only debug mode.

    def __init__(self, editor, view):
        super().__init__(editor, view)
        self._button_disable_timer = QTimer()
        self._button_disable_timer.setSingleShot(True)
        self._button_disable_timer.timeout.connect(self.disable_buttons)

    def actions(self):
        """
        Return an ordered list of actions provided by this module. An action
        is a name (also used to identify the icon) , description, and handler.
        """
        return [
            {
                "name": "stop",
                "display_name": _("Stop"),
                "description": _("Stop the running code."),
                "handler": self.button_stop,
                "shortcut": "Shift+F5",
            },
            {
                "name": "run",
                "display_name": _("Continue"),
                "description": _("Continue to run your Python script."),
                "handler": self.button_continue,
                "shortcut": "F5",
            },
            {
                "name": "step-over",
                "display_name": _("Step Over"),
                "description": _("Step over a line of code."),
                "handler": self.button_step_over,
                "shortcut": "F10",
            },
            {
                "name": "step-in",
                "display_name": _("Step In"),
                "description": _("Step into a function."),
                "handler": self.button_step_in,
                "shortcut": "F11",
            },
            {
                "name": "step-out",
                "display_name": _("Step Out"),
                "description": _("Step out of a function."),
                "handler": self.button_step_out,
                "shortcut": "Shift+F11",
            },
        ]

    def api(self):
        """
        Return a list of API specifications to be used by auto-suggest and call
        tips.
        """
        return []

    def start(self):
        """
        Start debugging the current script.
        """
        # Grab the Python file.
        tab = self.view.current_tab
        if tab is None:
            logger.debug("There is no active text editor.")
            self.stop()
            return
        if tab.path is None:
            # Unsaved file.
            self.editor.save()
        if tab.path:
            # If needed, save the script.
            if tab.isModified():
                self.editor.save_tab_to_file(tab)
            logger.debug(tab.text())
            self.set_buttons(modes=False)
            envars = self.editor.envars
            cwd = os.path.dirname(tab.path)
            self.runner = self.view.add_python3_runner(
                venv.interpreter, tab.path, cwd, debugger=True, envars=envars
            )
            self.runner.process.waitForStarted()
            self.runner.process.finished.connect(self.finished)
            self.view.add_debug_inspector()
            self.view.set_read_only(True)
            self.debugger = Debugger(
                "localhost", DEBUGGER_PORT, proc=self.runner.process
            )
            self.debugger.view = self
            self.debugger.start()
        else:
            logger.debug("Current script has not been saved. Aborting debug.")
            self.stop()

    def stop(self):
        """
        Stop the debug runner and reset the UI.
        """
        logger.debug("Stopping debugger.")
        if self.runner:
            self.runner.stop_process()
            self.runner = None
            self.debugger = None
            self.view.remove_python_runner()
            self.view.remove_debug_inspector()
        self.set_buttons(modes=True)
        self.editor.change_mode("python")
        self.editor.mode = "python"
        self.view.set_read_only(False)

    def finished(self):
        """
        Called when the debugged Python process is finished.
        """
        self.disable_buttons()
        self.editor.show_status_message(_("Your script has finished running."))
        for tab in self.view.widgets:
            tab.setSelection(0, 0, 0, 0)
            tab.reset_debugger_highlight()

    def _enable_buttons(self, *, enable):
        """
        Enable/disable all debug control buttons except 'stop'.
        """
        buttons = {
            action["name"]: enable
            for action in self.actions()
            if action["name"] != "stop"
        }
        self.set_buttons(**buttons)

    def disable_buttons(self):
        """
        Disable all debug control buttons except 'stop'.
        """
        self._enable_buttons(enable=False)

    def disable_buttons_later(self, *, milliseconds=100):
        """
        Set a timer to disable all debug control buttons except 'stop'.
        """
        self._button_disable_timer.start(milliseconds)

    def enable_buttons(self):
        """
        Enable all debug control buttons except 'stop': if the timer started
        in `disable_buttons_later` is active, stops it and does nothing else.
        """
        if self._button_disable_timer.isActive():
            self._button_disable_timer.stop()
            return
        self._enable_buttons(enable=True)

    def button_stop(self, event):
        """
        Button clicked to stop the current script and return to Python3 mode.
        """
        self.stop()

    def button_continue(self, event):
        """
        Button clicked to continue running the script.
        """
        self.disable_buttons_later()
        self.view.current_tab.reset_debugger_highlight()
        self.debugger.do_run()

    def button_step_over(self, event):
        """
        Button clicked to step over the current line of code.
        """
        self.disable_buttons_later()
        self.view.current_tab.reset_debugger_highlight()
        self.debugger.do_next()

    def button_step_in(self, event):
        """
        Button clicked to step into the current block of code.
        """
        self.disable_buttons_later()
        self.view.current_tab.reset_debugger_highlight()
        self.debugger.do_step()

    def button_step_out(self, event):
        """
        Button clicked to step out of the current block of code.
        """
        self.disable_buttons_later()
        self.view.current_tab.reset_debugger_highlight()
        self.debugger.do_return()

    def toggle_breakpoint(self, line, tab):
        """
        Toggle a breakpoint in the debugger.
        """
        bps = self.debugger.breakpoints(tab.path)
        breakpoint = bps.get(line + 1, None)
        if tab.markersAtLine(line):
            if breakpoint:
                self.debugger.disable_breakpoint(breakpoint)
            tab.markerDelete(line, tab.BREAKPOINT_MARKER)
        else:
            handle = tab.markerAdd(line, tab.BREAKPOINT_MARKER)
            tab.breakpoint_handles.add(handle)
            if breakpoint:
                self.debugger.enable_breakpoint(breakpoint)
            else:
                self.debugger.create_breakpoint(tab.path, line + 1)

    def debug_on_fail(self, message):
        """
        Called when, for any reason, the debug client was unable to connect to
        the debug runner. On a Raspberry Pi this is usually because it's an
        underpowereed machine and it takes time to start the debug runner
        process. (However, the debug client waits for 10 seconds for the
        runner to start.)
        """
        # Report the problem.
        process_runner = self.view.process_runner
        if process_runner:
            msg = _("Unable to connect to the Python debugger.\n\n") + message
            process_runner.append(msg.encode("utf-8"))
            # Set the state to finished.
            self.finished()
            process_runner.finished(1, -1)

    def debug_on_bootstrap(self):
        """
        Once the debugger is bootstrapped ensure all the current breakpoints
        are set. Do not set breakpoints (and remove the marker) if:

        * The marker is not visible (the line is -1)
        * The marker is not a duplicate of an existing line.
        * The line with the marker is not a valid breakpoint line.
        """
        for tab in self.view.widgets:
            break_lines = set()
            for handle in list(tab.breakpoint_handles):
                line = tab.markerLine(handle)
                code = tab.text(line)
                if line > -1 and line not in break_lines and is_breakpoint_line(code):
                    self.debugger.create_breakpoint(tab.path, line + 1)
                    break_lines.add(line)
                else:
                    tab.breakpoint_handles.remove(handle)
                    tab.markerDelete(line, -1)
        # Start the script running.
        self.debugger.do_run()

    def debug_on_breakpoint_enable(self, breakpoint):
        """
        Handle when a breakpoint is enabled.
        """
        tab = self.view.current_tab
        if tab.path == breakpoint.filename and not tab.markersAtLine(
            breakpoint.line - 1
        ):
            tab.markerAdd(breakpoint.line - 1, tab.BREAKPOINT_MARKER)

    def debug_on_breakpoint_disable(self, breakpoint):
        """
        Handle when a breakpoint is disabled.
        """
        tab = self.view.current_tab
        tab.markerDelete(breakpoint.line - 1, tab.BREAKPOINT_MARKER)

    def debug_on_line(self, filename, line):
        """
        Handle when the debugger has moved to the referenced line in the file.
        """
        self.enable_buttons()
        ignored = ["bdb.py"]  # Files the debugger should ignore.
        if os.path.basename(filename) in ignored:
            self.debugger.do_return()
            return
        self.view.current_tab.setSelection(0, 0, 0, 0)
        tab = self.editor.get_tab(filename)
        tab.debugger_at_line(line - 1)

    def debug_on_stack(self, stack):
        """
        Handle when the debugger sends an updated stack.
        """
        if stack:
            locals_dict = {}
            for frame in stack:
                for k, v in frame[1]["locals"].items():
                    locals_dict[k] = v
            self.view.update_debug_inspector(locals_dict)

    def debug_on_postmortem(self, args, kwargs):
        """
        Handle when something catastrophic happens to the debugger.
        """
        process_runner = self.view.process_runner
        for item in args:
            process_runner.append(item.encode("utf-8"))
        for k, v in kwargs.items():
            msg = "{}: {}".format(k, v)
            process_runner.append(msg.encode("utf-8"))

    def debug_on_info(self, message):
        """
        Handle when the debugger sends an informative textual message.
        """
        self.editor.show_status_message(_("Debugger info: {}").format(message))

    def debug_on_warning(self, message):
        """
        Handle when the debugger sends a warning message.
        """
        self.editor.show_status_message(_("Debugger warning: {}").format(message))

    def debug_on_error(self, message):
        """
        Handle when the debugger sends an error message.
        """
        self.editor.show_status_message(_("Debugger error: {}").format(message))

    def debug_on_call(self, args):
        """
        Handle when the debugger has called a function with the referenced
        args. Make sure the debugger steps into the function.
        """
        self.debugger.do_step()

    def debug_on_return(self, return_value):
        """
        Handle when the debugger returns from a function call with the
        referenced return value. Make sure the debugger steps out of the
        function to the caller.
        """
        self.debugger.do_step()

    def debug_on_finished(self):
        """
        Called when the runner has completed running the script to be
        debugged.
        """
        self.finished()

    def debug_on_breakpoint_ignore(self, breakpoint, count):
        """
        Handle when a breakpoint is to be ignored by the debugger. Currently
        an unimplemented extra feature.
        """
        pass

    def debug_on_breakpoint_clear(self, breakpoint):
        """
        Handle the clearing of the referenced breakpoint. Currently an
        unimplemented extra feature.
        """
        pass

    def debug_on_restart(self):
        """
        Handle when the debugger restarts. Currently an unimplemented extra
        feature.
        """
        pass

    def debug_on_exception(self, name, value):
        """
        Handle when the debugger encounters a named exception with an
        associated value. Clear the highlighted line and allow the script to
        run until the end so the error message is printed to stdout.
        """
        self.view.current_tab.reset_debugger_highlight()
        self.debugger.do_run()

actions()

Return an ordered list of actions provided by this module. An action is a name (also used to identify the icon) , description, and handler.

Source code in mu/modes/debugger.py
def actions(self):
    """
    Return an ordered list of actions provided by this module. An action
    is a name (also used to identify the icon) , description, and handler.
    """
    return [
        {
            "name": "stop",
            "display_name": _("Stop"),
            "description": _("Stop the running code."),
            "handler": self.button_stop,
            "shortcut": "Shift+F5",
        },
        {
            "name": "run",
            "display_name": _("Continue"),
            "description": _("Continue to run your Python script."),
            "handler": self.button_continue,
            "shortcut": "F5",
        },
        {
            "name": "step-over",
            "display_name": _("Step Over"),
            "description": _("Step over a line of code."),
            "handler": self.button_step_over,
            "shortcut": "F10",
        },
        {
            "name": "step-in",
            "display_name": _("Step In"),
            "description": _("Step into a function."),
            "handler": self.button_step_in,
            "shortcut": "F11",
        },
        {
            "name": "step-out",
            "display_name": _("Step Out"),
            "description": _("Step out of a function."),
            "handler": self.button_step_out,
            "shortcut": "Shift+F11",
        },
    ]

api()

Return a list of API specifications to be used by auto-suggest and call tips.

Source code in mu/modes/debugger.py
def api(self):
    """
    Return a list of API specifications to be used by auto-suggest and call
    tips.
    """
    return []

button_continue(event)

Button clicked to continue running the script.

Source code in mu/modes/debugger.py
def button_continue(self, event):
    """
    Button clicked to continue running the script.
    """
    self.disable_buttons_later()
    self.view.current_tab.reset_debugger_highlight()
    self.debugger.do_run()

button_step_in(event)

Button clicked to step into the current block of code.

Source code in mu/modes/debugger.py
def button_step_in(self, event):
    """
    Button clicked to step into the current block of code.
    """
    self.disable_buttons_later()
    self.view.current_tab.reset_debugger_highlight()
    self.debugger.do_step()

button_step_out(event)

Button clicked to step out of the current block of code.

Source code in mu/modes/debugger.py
def button_step_out(self, event):
    """
    Button clicked to step out of the current block of code.
    """
    self.disable_buttons_later()
    self.view.current_tab.reset_debugger_highlight()
    self.debugger.do_return()

button_step_over(event)

Button clicked to step over the current line of code.

Source code in mu/modes/debugger.py
def button_step_over(self, event):
    """
    Button clicked to step over the current line of code.
    """
    self.disable_buttons_later()
    self.view.current_tab.reset_debugger_highlight()
    self.debugger.do_next()

button_stop(event)

Button clicked to stop the current script and return to Python3 mode.

Source code in mu/modes/debugger.py
def button_stop(self, event):
    """
    Button clicked to stop the current script and return to Python3 mode.
    """
    self.stop()

debug_on_bootstrap()

Once the debugger is bootstrapped ensure all the current breakpoints are set. Do not set breakpoints (and remove the marker) if:

  • The marker is not visible (the line is -1)
  • The marker is not a duplicate of an existing line.
  • The line with the marker is not a valid breakpoint line.
Source code in mu/modes/debugger.py
def debug_on_bootstrap(self):
    """
    Once the debugger is bootstrapped ensure all the current breakpoints
    are set. Do not set breakpoints (and remove the marker) if:

    * The marker is not visible (the line is -1)
    * The marker is not a duplicate of an existing line.
    * The line with the marker is not a valid breakpoint line.
    """
    for tab in self.view.widgets:
        break_lines = set()
        for handle in list(tab.breakpoint_handles):
            line = tab.markerLine(handle)
            code = tab.text(line)
            if line > -1 and line not in break_lines and is_breakpoint_line(code):
                self.debugger.create_breakpoint(tab.path, line + 1)
                break_lines.add(line)
            else:
                tab.breakpoint_handles.remove(handle)
                tab.markerDelete(line, -1)
    # Start the script running.
    self.debugger.do_run()

debug_on_breakpoint_clear(breakpoint)

Handle the clearing of the referenced breakpoint. Currently an unimplemented extra feature.

Source code in mu/modes/debugger.py
def debug_on_breakpoint_clear(self, breakpoint):
    """
    Handle the clearing of the referenced breakpoint. Currently an
    unimplemented extra feature.
    """
    pass

debug_on_breakpoint_disable(breakpoint)

Handle when a breakpoint is disabled.

Source code in mu/modes/debugger.py
def debug_on_breakpoint_disable(self, breakpoint):
    """
    Handle when a breakpoint is disabled.
    """
    tab = self.view.current_tab
    tab.markerDelete(breakpoint.line - 1, tab.BREAKPOINT_MARKER)

debug_on_breakpoint_enable(breakpoint)

Handle when a breakpoint is enabled.

Source code in mu/modes/debugger.py
def debug_on_breakpoint_enable(self, breakpoint):
    """
    Handle when a breakpoint is enabled.
    """
    tab = self.view.current_tab
    if tab.path == breakpoint.filename and not tab.markersAtLine(
        breakpoint.line - 1
    ):
        tab.markerAdd(breakpoint.line - 1, tab.BREAKPOINT_MARKER)

debug_on_breakpoint_ignore(breakpoint, count)

Handle when a breakpoint is to be ignored by the debugger. Currently an unimplemented extra feature.

Source code in mu/modes/debugger.py
def debug_on_breakpoint_ignore(self, breakpoint, count):
    """
    Handle when a breakpoint is to be ignored by the debugger. Currently
    an unimplemented extra feature.
    """
    pass

debug_on_call(args)

Handle when the debugger has called a function with the referenced args. Make sure the debugger steps into the function.

Source code in mu/modes/debugger.py
def debug_on_call(self, args):
    """
    Handle when the debugger has called a function with the referenced
    args. Make sure the debugger steps into the function.
    """
    self.debugger.do_step()

debug_on_error(message)

Handle when the debugger sends an error message.

Source code in mu/modes/debugger.py
def debug_on_error(self, message):
    """
    Handle when the debugger sends an error message.
    """
    self.editor.show_status_message(_("Debugger error: {}").format(message))

debug_on_exception(name, value)

Handle when the debugger encounters a named exception with an associated value. Clear the highlighted line and allow the script to run until the end so the error message is printed to stdout.

Source code in mu/modes/debugger.py
def debug_on_exception(self, name, value):
    """
    Handle when the debugger encounters a named exception with an
    associated value. Clear the highlighted line and allow the script to
    run until the end so the error message is printed to stdout.
    """
    self.view.current_tab.reset_debugger_highlight()
    self.debugger.do_run()

debug_on_fail(message)

Called when, for any reason, the debug client was unable to connect to the debug runner. On a Raspberry Pi this is usually because it's an underpowereed machine and it takes time to start the debug runner process. (However, the debug client waits for 10 seconds for the runner to start.)

Source code in mu/modes/debugger.py
def debug_on_fail(self, message):
    """
    Called when, for any reason, the debug client was unable to connect to
    the debug runner. On a Raspberry Pi this is usually because it's an
    underpowereed machine and it takes time to start the debug runner
    process. (However, the debug client waits for 10 seconds for the
    runner to start.)
    """
    # Report the problem.
    process_runner = self.view.process_runner
    if process_runner:
        msg = _("Unable to connect to the Python debugger.\n\n") + message
        process_runner.append(msg.encode("utf-8"))
        # Set the state to finished.
        self.finished()
        process_runner.finished(1, -1)

debug_on_finished()

Called when the runner has completed running the script to be debugged.

Source code in mu/modes/debugger.py
def debug_on_finished(self):
    """
    Called when the runner has completed running the script to be
    debugged.
    """
    self.finished()

debug_on_info(message)

Handle when the debugger sends an informative textual message.

Source code in mu/modes/debugger.py
def debug_on_info(self, message):
    """
    Handle when the debugger sends an informative textual message.
    """
    self.editor.show_status_message(_("Debugger info: {}").format(message))

debug_on_line(filename, line)

Handle when the debugger has moved to the referenced line in the file.

Source code in mu/modes/debugger.py
def debug_on_line(self, filename, line):
    """
    Handle when the debugger has moved to the referenced line in the file.
    """
    self.enable_buttons()
    ignored = ["bdb.py"]  # Files the debugger should ignore.
    if os.path.basename(filename) in ignored:
        self.debugger.do_return()
        return
    self.view.current_tab.setSelection(0, 0, 0, 0)
    tab = self.editor.get_tab(filename)
    tab.debugger_at_line(line - 1)

debug_on_postmortem(args, kwargs)

Handle when something catastrophic happens to the debugger.

Source code in mu/modes/debugger.py
def debug_on_postmortem(self, args, kwargs):
    """
    Handle when something catastrophic happens to the debugger.
    """
    process_runner = self.view.process_runner
    for item in args:
        process_runner.append(item.encode("utf-8"))
    for k, v in kwargs.items():
        msg = "{}: {}".format(k, v)
        process_runner.append(msg.encode("utf-8"))

debug_on_restart()

Handle when the debugger restarts. Currently an unimplemented extra feature.

Source code in mu/modes/debugger.py
def debug_on_restart(self):
    """
    Handle when the debugger restarts. Currently an unimplemented extra
    feature.
    """
    pass

debug_on_return(return_value)

Handle when the debugger returns from a function call with the referenced return value. Make sure the debugger steps out of the function to the caller.

Source code in mu/modes/debugger.py
def debug_on_return(self, return_value):
    """
    Handle when the debugger returns from a function call with the
    referenced return value. Make sure the debugger steps out of the
    function to the caller.
    """
    self.debugger.do_step()

debug_on_stack(stack)

Handle when the debugger sends an updated stack.

Source code in mu/modes/debugger.py
def debug_on_stack(self, stack):
    """
    Handle when the debugger sends an updated stack.
    """
    if stack:
        locals_dict = {}
        for frame in stack:
            for k, v in frame[1]["locals"].items():
                locals_dict[k] = v
        self.view.update_debug_inspector(locals_dict)

debug_on_warning(message)

Handle when the debugger sends a warning message.

Source code in mu/modes/debugger.py
def debug_on_warning(self, message):
    """
    Handle when the debugger sends a warning message.
    """
    self.editor.show_status_message(_("Debugger warning: {}").format(message))

disable_buttons()

Disable all debug control buttons except 'stop'.

Source code in mu/modes/debugger.py
def disable_buttons(self):
    """
    Disable all debug control buttons except 'stop'.
    """
    self._enable_buttons(enable=False)

disable_buttons_later(*, milliseconds=100)

Set a timer to disable all debug control buttons except 'stop'.

Source code in mu/modes/debugger.py
def disable_buttons_later(self, *, milliseconds=100):
    """
    Set a timer to disable all debug control buttons except 'stop'.
    """
    self._button_disable_timer.start(milliseconds)

enable_buttons()

Enable all debug control buttons except 'stop': if the timer started in disable_buttons_later is active, stops it and does nothing else.

Source code in mu/modes/debugger.py
def enable_buttons(self):
    """
    Enable all debug control buttons except 'stop': if the timer started
    in `disable_buttons_later` is active, stops it and does nothing else.
    """
    if self._button_disable_timer.isActive():
        self._button_disable_timer.stop()
        return
    self._enable_buttons(enable=True)

finished()

Called when the debugged Python process is finished.

Source code in mu/modes/debugger.py
def finished(self):
    """
    Called when the debugged Python process is finished.
    """
    self.disable_buttons()
    self.editor.show_status_message(_("Your script has finished running."))
    for tab in self.view.widgets:
        tab.setSelection(0, 0, 0, 0)
        tab.reset_debugger_highlight()

start()

Start debugging the current script.

Source code in mu/modes/debugger.py
def start(self):
    """
    Start debugging the current script.
    """
    # Grab the Python file.
    tab = self.view.current_tab
    if tab is None:
        logger.debug("There is no active text editor.")
        self.stop()
        return
    if tab.path is None:
        # Unsaved file.
        self.editor.save()
    if tab.path:
        # If needed, save the script.
        if tab.isModified():
            self.editor.save_tab_to_file(tab)
        logger.debug(tab.text())
        self.set_buttons(modes=False)
        envars = self.editor.envars
        cwd = os.path.dirname(tab.path)
        self.runner = self.view.add_python3_runner(
            venv.interpreter, tab.path, cwd, debugger=True, envars=envars
        )
        self.runner.process.waitForStarted()
        self.runner.process.finished.connect(self.finished)
        self.view.add_debug_inspector()
        self.view.set_read_only(True)
        self.debugger = Debugger(
            "localhost", DEBUGGER_PORT, proc=self.runner.process
        )
        self.debugger.view = self
        self.debugger.start()
    else:
        logger.debug("Current script has not been saved. Aborting debug.")
        self.stop()

stop()

Stop the debug runner and reset the UI.

Source code in mu/modes/debugger.py
def stop(self):
    """
    Stop the debug runner and reset the UI.
    """
    logger.debug("Stopping debugger.")
    if self.runner:
        self.runner.stop_process()
        self.runner = None
        self.debugger = None
        self.view.remove_python_runner()
        self.view.remove_debug_inspector()
    self.set_buttons(modes=True)
    self.editor.change_mode("python")
    self.editor.mode = "python"
    self.view.set_read_only(False)

toggle_breakpoint(line, tab)

Toggle a breakpoint in the debugger.

Source code in mu/modes/debugger.py
def toggle_breakpoint(self, line, tab):
    """
    Toggle a breakpoint in the debugger.
    """
    bps = self.debugger.breakpoints(tab.path)
    breakpoint = bps.get(line + 1, None)
    if tab.markersAtLine(line):
        if breakpoint:
            self.debugger.disable_breakpoint(breakpoint)
        tab.markerDelete(line, tab.BREAKPOINT_MARKER)
    else:
        handle = tab.markerAdd(line, tab.BREAKPOINT_MARKER)
        tab.breakpoint_handles.add(handle)
        if breakpoint:
            self.debugger.enable_breakpoint(breakpoint)
        else:
            self.debugger.create_breakpoint(tab.path, line + 1)

mu.modes.pygamezero

The Pygame Zero / pygame mode.

mu.modes.pygamezero

The Pygame Zero mode for the Mu editor.

Copyright (c) 2015-2017 Nicholas H.Tollervey and others (see the AUTHORS file).

This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/.

PyGameZeroMode

Bases: BaseMode

Represents the functionality required by the PyGameZero mode.

Source code in mu/modes/pygamezero.py
class PyGameZeroMode(BaseMode):
    """
    Represents the functionality required by the PyGameZero mode.
    """

    name = _("Pygame Zero")
    short_name = "pygamezero"
    description = _("Make games with Pygame Zero.")
    icon = "pygamezero"
    runner = None
    builtins = [
        "clock",
        "music",
        "Actor",
        "keyboard",
        "animate",
        "Rect",
        "ZRect",
        "images",
        "sounds",
        "mouse",
        "keys",
        "keymods",
        "exit",
        "screen",
    ]

    def actions(self):
        """
        Return an ordered list of actions provided by this module. An action
        is a name (also used to identify the icon) , description, and handler.
        """
        return [
            {
                "name": "play",
                "display_name": _("Play"),
                "description": _("Play your Pygame Zero game."),
                "handler": self.play_toggle,
                "shortcut": "F5",
            },
            {
                "name": "images",
                "display_name": _("Images"),
                "description": _("Show the images used by Pygame Zero."),
                "handler": self.show_images,
                "shortcut": "Ctrl+Shift+I",
            },
            {
                "name": "fonts",
                "display_name": _("Fonts"),
                "description": _("Show the fonts used by Pygame Zero."),
                "handler": self.show_fonts,
                "shortcut": "Ctrl+Shift+F",
            },
            {
                "name": "sounds",
                "display_name": _("Sounds"),
                "description": _("Show the sounds used by Pygame Zero."),
                "handler": self.show_sounds,
                "shortcut": "Ctrl+Shift+N",
            },
            {
                "name": "music",
                "display_name": _("Music"),
                "description": _("Show the music used by Pygame Zero."),
                "handler": self.show_music,
                "shortcut": "Ctrl+Shift+M",
            },
        ]

    def api(self):
        """
        Return a list of API specifications to be used by auto-suggest and call
        tips.
        """
        return SHARED_APIS + PYTHON3_APIS + PI_APIS + PYGAMEZERO_APIS

    def play_toggle(self, event):
        """
        Handles the toggling of the play button to start/stop a script.
        """
        if self.runner:
            self.stop_game()
            play_slot = self.view.button_bar.slots["play"]
            play_slot.setIcon(load_icon("play"))
            play_slot.setText(_("Play"))
            play_slot.setToolTip(_("Play your Pygame Zero game."))
            self.set_buttons(modes=True)
        else:
            self.run_game()
            if self.runner:
                play_slot = self.view.button_bar.slots["play"]
                play_slot.setIcon(load_icon("stop"))
                play_slot.setText(_("Stop"))
                play_slot.setToolTip(_("Stop your Pygame Zero game."))
                self.set_buttons(modes=False)

    def run_game(self):
        """
        Run the current game.
        """
        # Grab the Python file.
        tab = self.view.current_tab
        if tab is None:
            logger.debug("There is no active text editor.")
            self.stop_game()
            return
        if tab.path is None:
            # Unsaved file.
            self.editor.save()
        if tab.path:
            # If needed, save the script.
            if tab.isModified():
                self.editor.save_tab_to_file(tab)
            logger.debug(tab.text())
            envars = self.editor.envars
            args = ["-m", "pgzero"]
            cwd = os.path.dirname(tab.path)

            self.runner = self.view.add_python3_runner(
                interpreter=venv.interpreter,
                script_name=tab.path,
                working_directory=cwd,
                interactive=False,
                envars=envars,
                python_args=args,
            )
            self.runner.process.waitForStarted()

    def stop_game(self):
        """
        Stop the currently running game.
        """
        logger.debug("Stopping script.")
        if self.runner:
            self.runner.stop_process()
            self.runner = None
        self.view.remove_python_runner()

    def show_images(self, event):
        """
        Open the directory containing the image assets used by Pygame Zero.

        This should open the host OS's file system explorer so users can drag
        new files into the opened folder.
        """
        self.view.open_directory_from_os(self.assets_dir("images"))

    def show_fonts(self, event):
        """
        Open the directory containing the font assets used by Pygame Zero.

        This should open the host OS's file system explorer so users can drag
        new files into the opened folder.
        """
        self.view.open_directory_from_os(self.assets_dir("fonts"))

    def show_sounds(self, event):
        """
        Open the directory containing the sound assets used by Pygame Zero.

        This should open the host OS's file system explorer so users can drag
        new files into the opened folder.
        """
        self.view.open_directory_from_os(self.assets_dir("sounds"))

    def show_music(self, event):
        """
        Open the directory containing the music assets used by Pygame Zero.

        This should open the host OS's file system explorer so users can drag
        new files into the opened folder.
        """
        self.view.open_directory_from_os(self.assets_dir("music"))

actions()

Return an ordered list of actions provided by this module. An action is a name (also used to identify the icon) , description, and handler.

Source code in mu/modes/pygamezero.py
def actions(self):
    """
    Return an ordered list of actions provided by this module. An action
    is a name (also used to identify the icon) , description, and handler.
    """
    return [
        {
            "name": "play",
            "display_name": _("Play"),
            "description": _("Play your Pygame Zero game."),
            "handler": self.play_toggle,
            "shortcut": "F5",
        },
        {
            "name": "images",
            "display_name": _("Images"),
            "description": _("Show the images used by Pygame Zero."),
            "handler": self.show_images,
            "shortcut": "Ctrl+Shift+I",
        },
        {
            "name": "fonts",
            "display_name": _("Fonts"),
            "description": _("Show the fonts used by Pygame Zero."),
            "handler": self.show_fonts,
            "shortcut": "Ctrl+Shift+F",
        },
        {
            "name": "sounds",
            "display_name": _("Sounds"),
            "description": _("Show the sounds used by Pygame Zero."),
            "handler": self.show_sounds,
            "shortcut": "Ctrl+Shift+N",
        },
        {
            "name": "music",
            "display_name": _("Music"),
            "description": _("Show the music used by Pygame Zero."),
            "handler": self.show_music,
            "shortcut": "Ctrl+Shift+M",
        },
    ]

api()

Return a list of API specifications to be used by auto-suggest and call tips.

Source code in mu/modes/pygamezero.py
def api(self):
    """
    Return a list of API specifications to be used by auto-suggest and call
    tips.
    """
    return SHARED_APIS + PYTHON3_APIS + PI_APIS + PYGAMEZERO_APIS

play_toggle(event)

Handles the toggling of the play button to start/stop a script.

Source code in mu/modes/pygamezero.py
def play_toggle(self, event):
    """
    Handles the toggling of the play button to start/stop a script.
    """
    if self.runner:
        self.stop_game()
        play_slot = self.view.button_bar.slots["play"]
        play_slot.setIcon(load_icon("play"))
        play_slot.setText(_("Play"))
        play_slot.setToolTip(_("Play your Pygame Zero game."))
        self.set_buttons(modes=True)
    else:
        self.run_game()
        if self.runner:
            play_slot = self.view.button_bar.slots["play"]
            play_slot.setIcon(load_icon("stop"))
            play_slot.setText(_("Stop"))
            play_slot.setToolTip(_("Stop your Pygame Zero game."))
            self.set_buttons(modes=False)

run_game()

Run the current game.

Source code in mu/modes/pygamezero.py
def run_game(self):
    """
    Run the current game.
    """
    # Grab the Python file.
    tab = self.view.current_tab
    if tab is None:
        logger.debug("There is no active text editor.")
        self.stop_game()
        return
    if tab.path is None:
        # Unsaved file.
        self.editor.save()
    if tab.path:
        # If needed, save the script.
        if tab.isModified():
            self.editor.save_tab_to_file(tab)
        logger.debug(tab.text())
        envars = self.editor.envars
        args = ["-m", "pgzero"]
        cwd = os.path.dirname(tab.path)

        self.runner = self.view.add_python3_runner(
            interpreter=venv.interpreter,
            script_name=tab.path,
            working_directory=cwd,
            interactive=False,
            envars=envars,
            python_args=args,
        )
        self.runner.process.waitForStarted()

show_fonts(event)

Open the directory containing the font assets used by Pygame Zero.

This should open the host OS's file system explorer so users can drag new files into the opened folder.

Source code in mu/modes/pygamezero.py
def show_fonts(self, event):
    """
    Open the directory containing the font assets used by Pygame Zero.

    This should open the host OS's file system explorer so users can drag
    new files into the opened folder.
    """
    self.view.open_directory_from_os(self.assets_dir("fonts"))

show_images(event)

Open the directory containing the image assets used by Pygame Zero.

This should open the host OS's file system explorer so users can drag new files into the opened folder.

Source code in mu/modes/pygamezero.py
def show_images(self, event):
    """
    Open the directory containing the image assets used by Pygame Zero.

    This should open the host OS's file system explorer so users can drag
    new files into the opened folder.
    """
    self.view.open_directory_from_os(self.assets_dir("images"))

show_music(event)

Open the directory containing the music assets used by Pygame Zero.

This should open the host OS's file system explorer so users can drag new files into the opened folder.

Source code in mu/modes/pygamezero.py
def show_music(self, event):
    """
    Open the directory containing the music assets used by Pygame Zero.

    This should open the host OS's file system explorer so users can drag
    new files into the opened folder.
    """
    self.view.open_directory_from_os(self.assets_dir("music"))

show_sounds(event)

Open the directory containing the sound assets used by Pygame Zero.

This should open the host OS's file system explorer so users can drag new files into the opened folder.

Source code in mu/modes/pygamezero.py
def show_sounds(self, event):
    """
    Open the directory containing the sound assets used by Pygame Zero.

    This should open the host OS's file system explorer so users can drag
    new files into the opened folder.
    """
    self.view.open_directory_from_os(self.assets_dir("sounds"))

stop_game()

Stop the currently running game.

Source code in mu/modes/pygamezero.py
def stop_game(self):
    """
    Stop the currently running game.
    """
    logger.debug("Stopping script.")
    if self.runner:
        self.runner.stop_process()
        self.runner = None
    self.view.remove_python_runner()

mu.modes.python3

The Python 3 editing mode.

mu.modes.python3

The Python3 mode for the Mu editor.

Copyright (c) 2015-2017 Nicholas H.Tollervey and others (see the AUTHORS file).

This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/.

KernelRunner

Bases: QObject

Used to control the iPython kernel in a non-blocking manner so the UI remains responsive.

Source code in mu/modes/python3.py
class KernelRunner(QObject):
    """
    Used to control the iPython kernel in a non-blocking manner so the UI
    remains responsive.
    """

    kernel_started = pyqtSignal(QtKernelManager, QtKernelClient)
    kernel_finished = pyqtSignal()
    # Used to build context with user defined envars when running the REPL.
    default_envars = os.environ.copy()

    def __init__(self, kernel_name, cwd, envars):
        """
        Initialise the kernel runner with a name of a kernel specification, a
        target current working directory, any user-defined envars and the
        path for the currently active virtualenv's site-packages.
        """
        logger.debug(
            "About to create KernelRunner for %s, %s, %s ",
            kernel_name,
            cwd,
            envars,
        )
        super().__init__()
        self.kernel_name = kernel_name
        self.cwd = cwd
        self.envars = dict(envars)

    def start_kernel(self):
        """
        Create the expected context, start the kernel, obtain a client and
        emit a signal when both are started.
        """
        logger.debug("About to start kernel")
        logger.info(sys.path)
        os.chdir(self.cwd)  # Ensure the kernel runs with the expected CWD.
        # Activate the user's virtual environment for the kernel subprocess
        # (VIRTUAL_ENV + the env's bin/Scripts prepended to PATH), mirroring
        # the Run path. User-defined envars below still take precedence. These
        # are restored to default_envars by stop_kernel.
        for name, value in venv.activation_vars().items():
            os.environ[name] = value
        # Add user defined envars to os.environ so they can be picked up by
        # the child process running the kernel.
        logger.info(
            "Starting iPython kernel with user defined envars: {}".format(self.envars)
        )
        for k, v in self.envars.items():
            if k != "PYTHONPATH":
                os.environ[k] = v

        self.repl_kernel_manager = MuKernelManager()
        self.repl_kernel_manager.kernel_name = self.kernel_name
        self.repl_kernel_manager.start_kernel()
        self.repl_kernel_client = self.repl_kernel_manager.client()
        self.kernel_started.emit(self.repl_kernel_manager, self.repl_kernel_client)

    def stop_kernel(self):
        """
        Clean up the context, stop the client connections to the kernel, affect
        an immediate shutdown of the kernel and emit a "finished" signal.
        """
        os.environ.clear()
        for k, v in self.default_envars.items():
            os.environ[k] = v
        self.repl_kernel_client.stop_channels()
        self.repl_kernel_manager.shutdown_kernel(now=True)
        self.kernel_finished.emit()

__init__(kernel_name, cwd, envars)

Initialise the kernel runner with a name of a kernel specification, a target current working directory, any user-defined envars and the path for the currently active virtualenv's site-packages.

Source code in mu/modes/python3.py
def __init__(self, kernel_name, cwd, envars):
    """
    Initialise the kernel runner with a name of a kernel specification, a
    target current working directory, any user-defined envars and the
    path for the currently active virtualenv's site-packages.
    """
    logger.debug(
        "About to create KernelRunner for %s, %s, %s ",
        kernel_name,
        cwd,
        envars,
    )
    super().__init__()
    self.kernel_name = kernel_name
    self.cwd = cwd
    self.envars = dict(envars)

start_kernel()

Create the expected context, start the kernel, obtain a client and emit a signal when both are started.

Source code in mu/modes/python3.py
def start_kernel(self):
    """
    Create the expected context, start the kernel, obtain a client and
    emit a signal when both are started.
    """
    logger.debug("About to start kernel")
    logger.info(sys.path)
    os.chdir(self.cwd)  # Ensure the kernel runs with the expected CWD.
    # Activate the user's virtual environment for the kernel subprocess
    # (VIRTUAL_ENV + the env's bin/Scripts prepended to PATH), mirroring
    # the Run path. User-defined envars below still take precedence. These
    # are restored to default_envars by stop_kernel.
    for name, value in venv.activation_vars().items():
        os.environ[name] = value
    # Add user defined envars to os.environ so they can be picked up by
    # the child process running the kernel.
    logger.info(
        "Starting iPython kernel with user defined envars: {}".format(self.envars)
    )
    for k, v in self.envars.items():
        if k != "PYTHONPATH":
            os.environ[k] = v

    self.repl_kernel_manager = MuKernelManager()
    self.repl_kernel_manager.kernel_name = self.kernel_name
    self.repl_kernel_manager.start_kernel()
    self.repl_kernel_client = self.repl_kernel_manager.client()
    self.kernel_started.emit(self.repl_kernel_manager, self.repl_kernel_client)

stop_kernel()

Clean up the context, stop the client connections to the kernel, affect an immediate shutdown of the kernel and emit a "finished" signal.

Source code in mu/modes/python3.py
def stop_kernel(self):
    """
    Clean up the context, stop the client connections to the kernel, affect
    an immediate shutdown of the kernel and emit a "finished" signal.
    """
    os.environ.clear()
    for k, v in self.default_envars.items():
        os.environ[k] = v
    self.repl_kernel_client.stop_channels()
    self.repl_kernel_manager.shutdown_kernel(now=True)
    self.kernel_finished.emit()

MuKernelManager

Bases: QtKernelManager

Source code in mu/modes/python3.py
class MuKernelManager(QtKernelManager):
    def start_kernel(self, **kw):
        """Starts a kernel on this host in a separate process.

        Subclassed to allow checking that the kernel uses the same Python as
        Mu itself.
        """
        kernel_cmd, kw = self.pre_start_kernel(**kw)
        cmd_interpreter = kernel_cmd[0]
        if cmd_interpreter != venv.interpreter:
            self.log.debug("Wrong interpreter selected to run REPL: %s", kernel_cmd)
            self.log.debug(
                "Using default interpreter to run REPL instead: %s",
                cmd_interpreter,
            )
            cmd_interpreter = venv.interpreter
            kernel_cmd[0] = cmd_interpreter

        # launch the kernel subprocess
        self.log.debug("Starting kernel: %s", kernel_cmd)
        self.kernel = self._launch_kernel(kernel_cmd, **kw)
        self.post_start_kernel(**kw)

start_kernel(**kw)

Starts a kernel on this host in a separate process.

Subclassed to allow checking that the kernel uses the same Python as Mu itself.

Source code in mu/modes/python3.py
def start_kernel(self, **kw):
    """Starts a kernel on this host in a separate process.

    Subclassed to allow checking that the kernel uses the same Python as
    Mu itself.
    """
    kernel_cmd, kw = self.pre_start_kernel(**kw)
    cmd_interpreter = kernel_cmd[0]
    if cmd_interpreter != venv.interpreter:
        self.log.debug("Wrong interpreter selected to run REPL: %s", kernel_cmd)
        self.log.debug(
            "Using default interpreter to run REPL instead: %s",
            cmd_interpreter,
        )
        cmd_interpreter = venv.interpreter
        kernel_cmd[0] = cmd_interpreter

    # launch the kernel subprocess
    self.log.debug("Starting kernel: %s", kernel_cmd)
    self.kernel = self._launch_kernel(kernel_cmd, **kw)
    self.post_start_kernel(**kw)

PythonMode

Bases: BaseMode

Represents the functionality required by the Python 3 mode.

Source code in mu/modes/python3.py
class PythonMode(BaseMode):
    """
    Represents the functionality required by the Python 3 mode.
    """

    name = _("Python 3")
    short_name = "python"
    description = _("Create code using standard Python 3.")
    icon = "python"
    runner = None
    has_debugger = True
    kernel_runner = None
    stop_kernel = pyqtSignal()

    def actions(self):
        """
        Return an ordered list of actions provided by this module. An action
        is a name (also used to identify the icon) , description, and handler.
        """
        buttons = [
            {
                "name": "run",
                "display_name": _("Run"),
                "description": _("Run your Python script."),
                "handler": self.run_toggle,
                "shortcut": "F5",
            },
            {
                "name": "debug",
                "display_name": _("Debug"),
                "description": _("Debug your Python script."),
                "handler": self.debug,
                "shortcut": "F6",
            },
            {
                "name": "repl",
                "display_name": _("REPL"),
                "description": _("Use the REPL for live coding."),
                "handler": self.toggle_repl,
                "shortcut": "Ctrl+Shift+I",
            },
        ]
        if CHARTS:
            buttons.append(
                {
                    "name": "plotter",
                    "display_name": _("Plotter"),
                    "description": _("Plot data from your script or the REPL."),
                    "handler": self.toggle_plotter,
                    "shortcut": "CTRL+Shift+P",
                }
            )
        return buttons

    def api(self):
        """
        Return a list of API specifications to be used by auto-suggest and call
        tips.
        """
        return SHARED_APIS + PYTHON3_APIS + PI_APIS

    def run_toggle(self, event):
        """
        Handles the toggling of the run button to start/stop a script.
        """
        run_slot = self.view.button_bar.slots["run"]
        if self.runner:
            self.stop_script()
            run_slot.setIcon(load_icon("run"))
            run_slot.setText(_("Run"))
            run_slot.setToolTip(_("Run your Python script."))
            self.set_buttons(debug=True, modes=True)
        else:
            self.run_script()
            if self.runner:
                # If the script started, toggle the button state. See #338.
                run_slot.setIcon(load_icon("stop"))
                run_slot.setText(_("Stop"))
                run_slot.setToolTip(_("Stop your Python script."))
                self.set_buttons(debug=False, modes=False)

    def run_script(self):
        """
        Run the current script.
        """
        # Grab the Python file.
        tab = self.view.current_tab
        if tab is None:
            logger.debug("There is no active text editor.")
            self.stop_script()
            return
        if tab.path is None:
            # Unsaved file.
            self.editor.save()
        if tab.path:
            # If needed, save the script.
            if tab.isModified():
                self.editor.save_tab_to_file(tab)
            envars = self.editor.envars
            cwd = os.path.dirname(tab.path)
            logger.info(
                "About to run script: %s",
                dict(
                    interpreter=venv.interpreter,
                    script_name=tab.path,
                    working_directory=cwd,
                    interactive=True,
                    envars=envars,
                ),
            )
            self.runner = self.view.add_python3_runner(
                interpreter=venv.interpreter,
                script_name=tab.path,
                working_directory=cwd,
                interactive=True,
                envars=envars,
            )
            self.runner.process.waitForStarted()
            if self.kernel_runner:
                self.set_buttons(plotter=False)
            elif self.plotter:
                self.set_buttons(repl=False)

    def stop_script(self):
        """
        Stop the currently running script.
        """
        logger.debug("Stopping script.")
        if self.runner:
            self.runner.stop_process()
            self.runner = None
        self.view.remove_python_runner()
        self.set_buttons(plotter=True, repl=True)
        self.return_focus_to_current_tab()

    def debug(self, event):
        """
        Debug the script using the debug mode.
        """
        logger.info("Starting debug mode.")
        self.editor.change_mode("debugger")
        self.editor.mode = "debugger"
        self.editor.modes["debugger"].start()

    def toggle_repl(self, event):
        """
        Toggles the REPL on and off
        """
        if self.kernel_runner is None:
            logger.info("Toggle REPL on.")
            self.editor.show_status_message(_("Starting iPython REPL."))
            self.add_repl()
        else:
            logger.info("Toggle REPL off.")
            self.editor.show_status_message(
                _("Stopping iPython REPL (this may take a short amount of time).")
            )
            self.remove_repl()

    def add_repl(self):
        """
        Create a new Jupyter REPL session in a non-blocking way.
        """
        self.set_buttons(repl=False)
        self.kernel_thread = QThread()
        self.kernel_runner = KernelRunner(
            kernel_name=venv.name,
            cwd=self.workspace_dir(),
            envars=self.editor.envars,
        )
        self.kernel_runner.moveToThread(self.kernel_thread)
        self.kernel_runner.kernel_started.connect(self.on_kernel_start)
        self.kernel_runner.kernel_finished.connect(self.kernel_thread.quit)
        self.stop_kernel.connect(self.kernel_runner.stop_kernel)
        self.kernel_thread.started.connect(self.kernel_runner.start_kernel)
        self.kernel_thread.finished.connect(self.on_kernel_stop)
        self.kernel_thread.start()

    def remove_repl(self):
        """
        Remove the Jupyter REPL session.
        """
        self.view.remove_repl()
        self.set_buttons(repl=False)
        # Don't block the GUI
        self.stop_kernel.emit()
        self.return_focus_to_current_tab()

    def toggle_plotter(self):
        """
        Toggles the plotter on and off.
        """
        if self.plotter:
            logger.info("Toggle plotter off.")
            self.remove_plotter()
        else:
            logger.info("Toggle plotter on.")
            self.add_plotter()

    def add_plotter(self):
        """
        Add a plotter pane.
        """
        self.view.add_python3_plotter(self)
        logger.info("Started plotter")
        self.plotter = True
        self.set_buttons(debug=False)
        if self.repl:
            self.set_buttons(run=False)
        elif self.runner:
            self.set_buttons(repl=False)

    def remove_plotter(self):
        """
        Remove the plotter pane, dump data and clean things up.
        """
        self.set_buttons(run=True, repl=True, debug=True)
        super().remove_plotter()

    def on_data_flood(self):
        """
        Ensure the process (REPL or runner) causing the data flood is stopped
        *before* the base on_data_flood is called to turn off the plotter and
        tell the user what to fix.
        """
        self.set_buttons(run=True, repl=True, debug=True)
        if self.kernel_runner:
            self.remove_repl()
        elif self.runner:
            self.run_toggle(None)
        super().on_data_flood()

    def on_kernel_start(self, kernel_manager, kernel_client):
        """
        Handles UI update when the kernel runner has started the iPython
        kernel.
        """
        self.view.add_jupyter_repl(kernel_manager, kernel_client)
        self.set_buttons(repl=True)
        if self.runner:
            self.set_buttons(plotter=False)
        elif self.plotter:
            self.set_buttons(run=False, debug=False)
        self.editor.show_status_message(_("REPL started."))

    def on_kernel_stop(self):
        """
        Handles UI updates for when the kernel runner has shut down the running
        iPython kernel.
        """
        self.repl_kernel_manager = None
        self.set_buttons(repl=True, plotter=True, run=True)
        self.editor.show_status_message(_("REPL stopped."))
        self.kernel_runner = None

actions()

Return an ordered list of actions provided by this module. An action is a name (also used to identify the icon) , description, and handler.

Source code in mu/modes/python3.py
def actions(self):
    """
    Return an ordered list of actions provided by this module. An action
    is a name (also used to identify the icon) , description, and handler.
    """
    buttons = [
        {
            "name": "run",
            "display_name": _("Run"),
            "description": _("Run your Python script."),
            "handler": self.run_toggle,
            "shortcut": "F5",
        },
        {
            "name": "debug",
            "display_name": _("Debug"),
            "description": _("Debug your Python script."),
            "handler": self.debug,
            "shortcut": "F6",
        },
        {
            "name": "repl",
            "display_name": _("REPL"),
            "description": _("Use the REPL for live coding."),
            "handler": self.toggle_repl,
            "shortcut": "Ctrl+Shift+I",
        },
    ]
    if CHARTS:
        buttons.append(
            {
                "name": "plotter",
                "display_name": _("Plotter"),
                "description": _("Plot data from your script or the REPL."),
                "handler": self.toggle_plotter,
                "shortcut": "CTRL+Shift+P",
            }
        )
    return buttons

add_plotter()

Add a plotter pane.

Source code in mu/modes/python3.py
def add_plotter(self):
    """
    Add a plotter pane.
    """
    self.view.add_python3_plotter(self)
    logger.info("Started plotter")
    self.plotter = True
    self.set_buttons(debug=False)
    if self.repl:
        self.set_buttons(run=False)
    elif self.runner:
        self.set_buttons(repl=False)

add_repl()

Create a new Jupyter REPL session in a non-blocking way.

Source code in mu/modes/python3.py
def add_repl(self):
    """
    Create a new Jupyter REPL session in a non-blocking way.
    """
    self.set_buttons(repl=False)
    self.kernel_thread = QThread()
    self.kernel_runner = KernelRunner(
        kernel_name=venv.name,
        cwd=self.workspace_dir(),
        envars=self.editor.envars,
    )
    self.kernel_runner.moveToThread(self.kernel_thread)
    self.kernel_runner.kernel_started.connect(self.on_kernel_start)
    self.kernel_runner.kernel_finished.connect(self.kernel_thread.quit)
    self.stop_kernel.connect(self.kernel_runner.stop_kernel)
    self.kernel_thread.started.connect(self.kernel_runner.start_kernel)
    self.kernel_thread.finished.connect(self.on_kernel_stop)
    self.kernel_thread.start()

api()

Return a list of API specifications to be used by auto-suggest and call tips.

Source code in mu/modes/python3.py
def api(self):
    """
    Return a list of API specifications to be used by auto-suggest and call
    tips.
    """
    return SHARED_APIS + PYTHON3_APIS + PI_APIS

debug(event)

Debug the script using the debug mode.

Source code in mu/modes/python3.py
def debug(self, event):
    """
    Debug the script using the debug mode.
    """
    logger.info("Starting debug mode.")
    self.editor.change_mode("debugger")
    self.editor.mode = "debugger"
    self.editor.modes["debugger"].start()

on_data_flood()

Ensure the process (REPL or runner) causing the data flood is stopped before the base on_data_flood is called to turn off the plotter and tell the user what to fix.

Source code in mu/modes/python3.py
def on_data_flood(self):
    """
    Ensure the process (REPL or runner) causing the data flood is stopped
    *before* the base on_data_flood is called to turn off the plotter and
    tell the user what to fix.
    """
    self.set_buttons(run=True, repl=True, debug=True)
    if self.kernel_runner:
        self.remove_repl()
    elif self.runner:
        self.run_toggle(None)
    super().on_data_flood()

on_kernel_start(kernel_manager, kernel_client)

Handles UI update when the kernel runner has started the iPython kernel.

Source code in mu/modes/python3.py
def on_kernel_start(self, kernel_manager, kernel_client):
    """
    Handles UI update when the kernel runner has started the iPython
    kernel.
    """
    self.view.add_jupyter_repl(kernel_manager, kernel_client)
    self.set_buttons(repl=True)
    if self.runner:
        self.set_buttons(plotter=False)
    elif self.plotter:
        self.set_buttons(run=False, debug=False)
    self.editor.show_status_message(_("REPL started."))

on_kernel_stop()

Handles UI updates for when the kernel runner has shut down the running iPython kernel.

Source code in mu/modes/python3.py
def on_kernel_stop(self):
    """
    Handles UI updates for when the kernel runner has shut down the running
    iPython kernel.
    """
    self.repl_kernel_manager = None
    self.set_buttons(repl=True, plotter=True, run=True)
    self.editor.show_status_message(_("REPL stopped."))
    self.kernel_runner = None

remove_plotter()

Remove the plotter pane, dump data and clean things up.

Source code in mu/modes/python3.py
def remove_plotter(self):
    """
    Remove the plotter pane, dump data and clean things up.
    """
    self.set_buttons(run=True, repl=True, debug=True)
    super().remove_plotter()

remove_repl()

Remove the Jupyter REPL session.

Source code in mu/modes/python3.py
def remove_repl(self):
    """
    Remove the Jupyter REPL session.
    """
    self.view.remove_repl()
    self.set_buttons(repl=False)
    # Don't block the GUI
    self.stop_kernel.emit()
    self.return_focus_to_current_tab()

run_script()

Run the current script.

Source code in mu/modes/python3.py
def run_script(self):
    """
    Run the current script.
    """
    # Grab the Python file.
    tab = self.view.current_tab
    if tab is None:
        logger.debug("There is no active text editor.")
        self.stop_script()
        return
    if tab.path is None:
        # Unsaved file.
        self.editor.save()
    if tab.path:
        # If needed, save the script.
        if tab.isModified():
            self.editor.save_tab_to_file(tab)
        envars = self.editor.envars
        cwd = os.path.dirname(tab.path)
        logger.info(
            "About to run script: %s",
            dict(
                interpreter=venv.interpreter,
                script_name=tab.path,
                working_directory=cwd,
                interactive=True,
                envars=envars,
            ),
        )
        self.runner = self.view.add_python3_runner(
            interpreter=venv.interpreter,
            script_name=tab.path,
            working_directory=cwd,
            interactive=True,
            envars=envars,
        )
        self.runner.process.waitForStarted()
        if self.kernel_runner:
            self.set_buttons(plotter=False)
        elif self.plotter:
            self.set_buttons(repl=False)

run_toggle(event)

Handles the toggling of the run button to start/stop a script.

Source code in mu/modes/python3.py
def run_toggle(self, event):
    """
    Handles the toggling of the run button to start/stop a script.
    """
    run_slot = self.view.button_bar.slots["run"]
    if self.runner:
        self.stop_script()
        run_slot.setIcon(load_icon("run"))
        run_slot.setText(_("Run"))
        run_slot.setToolTip(_("Run your Python script."))
        self.set_buttons(debug=True, modes=True)
    else:
        self.run_script()
        if self.runner:
            # If the script started, toggle the button state. See #338.
            run_slot.setIcon(load_icon("stop"))
            run_slot.setText(_("Stop"))
            run_slot.setToolTip(_("Stop your Python script."))
            self.set_buttons(debug=False, modes=False)

stop_script()

Stop the currently running script.

Source code in mu/modes/python3.py
def stop_script(self):
    """
    Stop the currently running script.
    """
    logger.debug("Stopping script.")
    if self.runner:
        self.runner.stop_process()
        self.runner = None
    self.view.remove_python_runner()
    self.set_buttons(plotter=True, repl=True)
    self.return_focus_to_current_tab()

toggle_plotter()

Toggles the plotter on and off.

Source code in mu/modes/python3.py
def toggle_plotter(self):
    """
    Toggles the plotter on and off.
    """
    if self.plotter:
        logger.info("Toggle plotter off.")
        self.remove_plotter()
    else:
        logger.info("Toggle plotter on.")
        self.add_plotter()

toggle_repl(event)

Toggles the REPL on and off

Source code in mu/modes/python3.py
def toggle_repl(self, event):
    """
    Toggles the REPL on and off
    """
    if self.kernel_runner is None:
        logger.info("Toggle REPL on.")
        self.editor.show_status_message(_("Starting iPython REPL."))
        self.add_repl()
    else:
        logger.info("Toggle REPL off.")
        self.editor.show_status_message(
            _("Stopping iPython REPL (this may take a short amount of time).")
        )
        self.remove_repl()

mu.resources

Contains utility functions for working with binary assets used by Mu (mainly images).

mu.resources

Copyright (c) 2015-2017 Nicholas H.Tollervey and others (see the AUTHORS file).

Based upon work done for Puppy IDE by Dan Pope, Nicholas Tollervey and Damien George.

This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/.

load_font_data(name)

Load the (binary) content of a font as bytes

Source code in mu/resources/__init__.py
def load_font_data(name):
    """
    Load the (binary) content of a font as bytes
    """
    return resource_bytes("fonts/" + name)

load_icon(name)

Load an icon from the resources directory.

Source code in mu/resources/__init__.py
def load_icon(name):
    """Load an icon from the resources directory."""
    svg_path = path(name, ext=".svg")
    if os.path.exists(svg_path):
        svg_icon = QIcon(svg_path)
        if svg_icon:
            return svg_icon
    return QIcon(path(name))

load_movie(name)

Load an animated GIF from the resources directory.

Source code in mu/resources/__init__.py
def load_movie(name):
    """Load an animated GIF from the resources directory."""
    return QMovie(path(name))

load_pixmap(name, size=None)

Load a pixmap from the resources directory.

Source code in mu/resources/__init__.py
def load_pixmap(name, size=None):
    """Load a pixmap from the resources directory."""
    if size is not None:
        icon = load_icon(name)
        return icon.pixmap(size)
    return QPixmap(path(name))

load_stylesheet(name)

Load a CSS stylesheet from the resources directory.

Source code in mu/resources/__init__.py
def load_stylesheet(name):
    """Load a CSS stylesheet from the resources directory."""
    return resource_bytes("css/" + name).decode("utf8")

path(name, resource_dir='images/', ext='')

Return the filename for the referenced image.

Source code in mu/resources/__init__.py
def path(name, resource_dir="images/", ext=""):
    """Return the filename for the referenced image."""
    return resource_filename(resource_dir + name + ext)

resource_bytes(subpath)

Return the (binary) contents of a resource in this package.

Source code in mu/resources/__init__.py
def resource_bytes(subpath):
    """Return the (binary) contents of a resource in this package."""
    return files(__name__).joinpath(subpath).read_bytes()

resource_filename(subpath)

Return the absolute filesystem path of a resource in this package.

Source code in mu/resources/__init__.py
def resource_filename(subpath):
    """Return the absolute filesystem path of a resource in this package."""
    return str(files(__name__).joinpath(subpath))