src/eric7/MicroPython/CircuitPythonUpdater/CircuitPythonUpdaterInterface.py

branch
eric7
changeset 9756
9854647c8c5c
parent 9755
1a09700229e7
child 9757
ab6e87f6f1c4
--- a/src/eric7/MicroPython/CircuitPythonUpdater/CircuitPythonUpdaterInterface.py	Sun Feb 12 18:11:20 2023 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,668 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright (c) 2023 Detlev Offenbach <detlev@die-offenbachs.de>
-#
-
-"""
-Module implementing an interface to the 'circup' package.
-"""
-
-import importlib
-import logging
-import os
-import re
-import shutil
-
-import requests
-
-from PyQt6.QtCore import QObject, pyqtSlot
-from PyQt6.QtWidgets import QDialog, QInputDialog, QLineEdit, QMenu
-
-from eric7 import Preferences
-from eric7.EricGui.EricOverrideCursor import EricOverrideCursor
-from eric7.EricWidgets import EricFileDialog, EricMessageBox
-from eric7.EricWidgets.EricApplication import ericApp
-from eric7.EricWidgets.EricListSelectionDialog import EricListSelectionDialog
-from eric7.SystemUtilities import PythonUtilities
-
-try:
-    import circup
-
-    circup.logger.setLevel(logging.WARNING)
-except ImportError:
-    circup = None
-
-
-class CircuitPythonUpdaterInterface(QObject):
-    """
-    Class implementing an interface to the 'circup' package.
-    """
-
-    def __init__(self, device, parent=None):
-        """
-        Constructor
-
-        @param device reference to the CircuitPython device interface
-        @type CircuitPythonDevice
-        @param parent reference to the parent object (defaults to None)
-        @type QObject (optional)
-        """
-        super().__init__(parent)
-
-        self.__device = device
-
-        self.__installMenu = QMenu(self.tr("Install Modules"))
-        self.__installMenu.setTearOffEnabled(True)
-        self.__installMenu.addAction(
-            self.tr("Select from Available Modules"), self.__installFromAvailable
-        )
-        self.__installMenu.addAction(
-            self.tr("Install Requirements"), self.__installRequirements
-        )
-        self.__installMenu.addAction(
-            self.tr("Install based on 'code.py'"), self.__installFromCode
-        )
-        self.__installMenu.addSeparator()
-        self.__installPyAct = self.__installMenu.addAction(
-            self.tr("Install Python Source")
-        )
-        self.__installPyAct.setCheckable(True)
-        self.__installPyAct.setChecked(False)
-        # kind of hack to make this action not hide the menu
-        # Note: parent menus are hidden nevertheless
-        self.__installPyAct.toggled.connect(self.__installMenu.show)
-
-    def populateMenu(self, menu):
-        """
-        Public method to populate the 'circup' menu.
-
-        @param menu reference to the menu to be populated
-        @type QMenu
-        """
-        from .CircupFunctions import patch_circup
-
-        patch_circup()
-        isMounted = self.__device.supportsLocalFileAccess()
-
-        act = menu.addAction(self.tr("circup"), self.__aboutCircup)
-        font = act.font()
-        font.setBold(True)
-        act.setFont(font)
-        menu.addSeparator()
-        menu.addAction(
-            self.tr("List Outdated Modules"), self.__listOutdatedModules
-        ).setEnabled(isMounted)
-        menu.addAction(self.tr("Update Modules"), self.__updateModules).setEnabled(
-            isMounted
-        )
-        menu.addAction(
-            self.tr("Update All Modules"), self.__updateAllModules
-        ).setEnabled(isMounted)
-        menu.addSeparator()
-        menu.addAction(self.tr("Show Available Modules"), self.__showAvailableModules)
-        menu.addAction(
-            self.tr("Show Installed Modules"), self.__showInstalledModules
-        ).setEnabled(isMounted)
-        menu.addMenu(self.__installMenu).setEnabled(isMounted)
-        menu.addAction(
-            self.tr("Uninstall Modules"), self.__uninstallModules
-        ).setEnabled(isMounted)
-        menu.addSeparator()
-        menu.addAction(
-            self.tr("Generate Requirements ..."), self.__generateRequirements
-        ).setEnabled(isMounted)
-        menu.addSeparator()
-        menu.addAction(self.tr("Show Bundles"), self.__showBundles)
-        menu.addAction(self.tr("Show Bundles with Modules"), self.__showBundlesModules)
-        menu.addSeparator()
-        menu.addAction(self.tr("Add Bundle"), self.__addBundle)
-        menu.addAction(self.tr("Remove Bundles"), self.__removeBundle)
-
-    @pyqtSlot()
-    def __aboutCircup(self):
-        """
-        Private slot to show some info about 'circup'.
-        """
-        version = circup.get_circup_version()
-        if version is None:
-            version = self.tr("unknown")
-
-        EricMessageBox.information(
-            None,
-            self.tr("About circup"),
-            self.tr(
-                """<p><b>circup Version {0}</b></p>"""
-                """<p><i>circup</i> is a tool to manage and update libraries on a"""
-                """ CircuitPython device.</p>""",
-            ).format(version),
-        )
-
-    @pyqtSlot()
-    def installCircup(self):
-        """
-        Public slot to install the 'circup' package via pip.
-        """
-        global circup
-
-        pip = ericApp().getObject("Pip")
-        pip.installPackages(
-            ["circup"], interpreter=PythonUtilities.getPythonExecutable()
-        )
-
-        circup = importlib.import_module("circup")
-        circup.logger.setLevel(logging.WARNING)
-
-    @pyqtSlot()
-    def __showBundles(self, withModules=False):
-        """
-        Private slot to show the available bundles (default and local).
-
-        @param withModules flag indicating to list the modules and their version
-            (defaults to False)
-        @type bool (optional)
-        """
-        from .ShowBundlesDialog import ShowBundlesDialog
-
-        with EricOverrideCursor():
-            dlg = ShowBundlesDialog(withModules=withModules)
-        dlg.exec()
-
-    @pyqtSlot()
-    def __showBundlesModules(self):
-        """
-        Private slot to show the available bundles (default and local) with their
-        modules.
-        """
-        self.__showBundles(withModules=True)
-
-    @pyqtSlot()
-    def __addBundle(self):
-        """
-        Private slot to add a bundle to the local bundles list, by "user/repo" github
-        string.
-        """
-        bundle, ok = QInputDialog.getText(
-            None,
-            self.tr("Add Bundle"),
-            self.tr("Enter Bundle by 'User/Repo' Github String:"),
-            QLineEdit.EchoMode.Normal,
-        )
-        if ok and bundle:
-            bundles = circup.get_bundles_local_dict()
-            modified = False
-
-            # do some cleanup
-            bundle = re.sub(r"https?://github.com/([^/]+/[^/]+)(/.*)?", r"\1", bundle)
-            if bundle in bundles:
-                EricMessageBox.information(
-                    None,
-                    self.tr("Add Bundle"),
-                    self.tr(
-                        """<p>The bundle <b>{0}</b> is already in the list.</p>"""
-                    ).format(bundle),
-                )
-                return
-
-            try:
-                cBundle = circup.Bundle(bundle)
-            except ValueError:
-                EricMessageBox.critical(
-                    None,
-                    self.tr("Add Bundle"),
-                    self.tr(
-                        """<p>The bundle string is invalid, expecting github URL"""
-                        """ or 'user/repository' string.</p>"""
-                    ),
-                )
-                return
-
-            result = requests.head("https://github.com/" + bundle)
-            if result.status_code == requests.codes.NOT_FOUND:
-                EricMessageBox.critical(
-                    None,
-                    self.tr("Add Bundle"),
-                    self.tr(
-                        """<p>The bundle string is invalid. The repository doesn't"""
-                        """ exist (error code 404).</p>"""
-                    ),
-                )
-                return
-
-            if not cBundle.validate():
-                EricMessageBox.critical(
-                    None,
-                    self.tr("Add Bundle"),
-                    self.tr(
-                        """<p>The bundle string is invalid. Is the repository a valid"""
-                        """ circup bundle?</p>"""
-                    ),
-                )
-                return
-
-            # Use the bundle string as the dictionary key for uniqueness
-            bundles[bundle] = bundle
-            modified = True
-            EricMessageBox.information(
-                None,
-                self.tr("Add Bundle"),
-                self.tr("""<p>Added bundle <b>{0}</b> ({1}).</p>""").format(
-                    bundle, cBundle.url
-                ),
-            )
-
-            if modified:
-                # save the bundles list
-                circup.save_local_bundles(bundles)
-                # update and get the new bundle for the first time
-                circup.get_bundle_versions(circup.get_bundles_list())
-
-    @pyqtSlot()
-    def __removeBundle(self):
-        """
-        Private slot to remove one or more bundles from the local bundles list.
-        """
-        localBundles = circup.get_bundles_local_dict()
-        dlg = EricListSelectionDialog(
-            sorted(localBundles.keys()),
-            title=self.tr("Remove Bundles"),
-            message=self.tr("Select the bundles to be removed:"),
-            checkBoxSelection=True,
-        )
-        modified = False
-        if dlg.exec() == QDialog.DialogCode.Accepted:
-            bundles = dlg.getSelection()
-            for bundle in bundles:
-                del localBundles[bundle]
-                modified = True
-
-        if modified:
-            circup.save_local_bundles(localBundles)
-            EricMessageBox.information(
-                None,
-                self.tr("Remove Bundles"),
-                self.tr(
-                    """<p>These bundles were removed from the local bundles list.{0}"""
-                    """</p>"""
-                ).format("""<ul><li>{0}</li></ul>""".format("</li><li>".join(bundles))),
-            )
-
-    @pyqtSlot()
-    def __listOutdatedModules(self):
-        """
-        Private slot to list the outdated modules of the connected device.
-        """
-        from .ShowOutdatedDialog import ShowOutdatedDialog
-
-        devicePath = self.__device.getWorkspace()
-
-        cpyVersion, board_id = circup.get_circuitpython_version(devicePath)
-        circup.CPY_VERSION = cpyVersion
-
-        with EricOverrideCursor():
-            dlg = ShowOutdatedDialog(devicePath=devicePath)
-        dlg.exec()
-
-    @pyqtSlot()
-    def __updateModules(self):
-        """
-        Private slot to update the modules of the connected device.
-        """
-        from .ShowOutdatedDialog import ShowOutdatedDialog
-
-        devicePath = self.__device.getWorkspace()
-
-        cpyVersion, board_id = circup.get_circuitpython_version(devicePath)
-        circup.CPY_VERSION = cpyVersion
-
-        with EricOverrideCursor():
-            dlg = ShowOutdatedDialog(devicePath=devicePath, selectionMode=True)
-        if dlg.exec() == QDialog.DialogCode.Accepted:
-            modules = dlg.getSelection()
-            self.__doUpdateModules(modules)
-
-    @pyqtSlot()
-    def __updateAllModules(self):
-        """
-        Private slot to update all modules of the connected device.
-        """
-        devicePath = self.__device.getWorkspace()
-
-        cpyVersion, board_id = circup.get_circuitpython_version(devicePath)
-        circup.CPY_VERSION = cpyVersion
-
-        with EricOverrideCursor():
-            modules = [
-                m
-                for m in circup.find_modules(devicePath, circup.get_bundles_list())
-                if m.outofdate
-            ]
-        if modules:
-            self.__doUpdateModules(modules)
-        else:
-            EricMessageBox.information(
-                None,
-                self.tr("Update Modules"),
-                self.tr("All modules are already up-to-date."),
-            )
-
-    def __doUpdateModules(self, modules):
-        """
-        Private method to perform the update of a list of modules.
-
-        @param modules list of modules to be updated
-        @type circup.Module
-        """
-        updatedModules = []
-        for module in modules:
-            try:
-                module.update()
-                updatedModules.append(module.name)
-            except Exception as ex:
-                EricMessageBox.critical(
-                    None,
-                    self.tr("Update Modules"),
-                    self.tr(
-                        """<p>There was an error updating <b>{0}</b>.</p>"""
-                        """<p>Error: {1}</p>"""
-                    ).format(module.name, str(ex)),
-                )
-
-        if updatedModules:
-            EricMessageBox.information(
-                None,
-                self.tr("Update Modules"),
-                self.tr(
-                    """<p>These modules were updated on the connected device.{0}</p>"""
-                ).format(
-                    """<ul><li>{0}</li></ul>""".format("</li><li>".join(updatedModules))
-                ),
-            )
-        else:
-            EricMessageBox.information(
-                None,
-                self.tr("Update Modules"),
-                self.tr("No modules could be updated."),
-            )
-
-    @pyqtSlot()
-    def __showAvailableModules(self):
-        """
-        Private slot to show the available modules.
-
-        These are modules which could be installed on the device.
-        """
-        from ..ShowModulesDialog import ShowModulesDialog
-
-        with EricOverrideCursor():
-            availableModules = circup.get_bundle_versions(circup.get_bundles_list())
-            moduleNames = [m.replace(".py", "") for m in availableModules]
-
-        dlg = ShowModulesDialog(moduleNames)
-        dlg.exec()
-
-    @pyqtSlot()
-    def __showInstalledModules(self):
-        """
-        Private slot to show the modules installed on the connected device.
-        """
-        from .ShowInstalledDialog import ShowInstalledDialog
-
-        devicePath = self.__device.getWorkspace()
-
-        with EricOverrideCursor():
-            dlg = ShowInstalledDialog(devicePath=devicePath)
-        dlg.exec()
-
-    @pyqtSlot()
-    def __installFromAvailable(self):
-        """
-        Private slot to install modules onto the connected device.
-        """
-        from ..ShowModulesDialog import ShowModulesDialog
-
-        with EricOverrideCursor():
-            availableModules = circup.get_bundle_versions(circup.get_bundles_list())
-            moduleNames = [m.replace(".py", "") for m in availableModules]
-
-        dlg = ShowModulesDialog(moduleNames, selectionMode=True)
-        if dlg.exec() == QDialog.DialogCode.Accepted:
-            modules = dlg.getSelection()
-            self.__installModules(modules)
-
-    @pyqtSlot()
-    def __installRequirements(self):
-        """
-        Private slot to install modules determined by a requirements file.
-        """
-        homeDir = (
-            Preferences.getMicroPython("MpyWorkspace")
-            or Preferences.getMultiProject("Workspace")
-            or os.path.expanduser("~")
-        )
-        reqFile = EricFileDialog.getOpenFileName(
-            None,
-            self.tr("Install Modules"),
-            homeDir,
-            self.tr("Text Files (*.txt);;All Files (*)"),
-        )
-        if reqFile:
-            if os.path.exists(reqFile):
-                with open(reqFile, "r") as fp:
-                    requirementsText = fp.read()
-                modules = circup.libraries_from_requirements(requirementsText)
-                if modules:
-                    self.__installModules(modules)
-                else:
-                    EricMessageBox.critical(
-                        None,
-                        self.tr("Install Modules"),
-                        self.tr(
-                            """<p>The given requirements file <b>{0}</b> does not"""
-                            """ contain valid modules.</p>"""
-                        ).format(reqFile),
-                    )
-            else:
-                EricMessageBox.critical(
-                    None,
-                    self.tr("Install Modules"),
-                    self.tr(
-                        """<p>The given requirements file <b>{0}</b> does not exist."""
-                        """</p>"""
-                    ).format(reqFile),
-                )
-
-    @pyqtSlot()
-    def __installFromCode(self):
-        """
-        Private slot to install modules based on the 'code.py' file of the
-        connected device.
-        """
-        devicePath = self.__device.getWorkspace()
-
-        codeFile = EricFileDialog.getOpenFileName(
-            None,
-            self.tr("Install Modules"),
-            os.path.join(devicePath, "code.py"),
-            self.tr("Python Files (*.py);;All Files (*)"),
-        )
-        if codeFile:
-            if os.path.exists(codeFile):
-                with EricOverrideCursor():
-                    availableModules = circup.get_bundle_versions(
-                        circup.get_bundles_list()
-                    )
-                    moduleNames = {}
-                    for module, metadata in availableModules.items():
-                        moduleNames[module.replace(".py", "")] = metadata
-
-                modules = circup.libraries_from_imports(codeFile, moduleNames)
-                if modules:
-                    self.__installModules(modules)
-                else:
-                    EricMessageBox.critical(
-                        None,
-                        self.tr("Install Modules"),
-                        self.tr(
-                            """<p>The given code file <b>{0}</b> does not"""
-                            """ contain valid import statements or does not import"""
-                            """ external modules.</p>"""
-                        ).format(codeFile),
-                    )
-            else:
-                EricMessageBox.critical(
-                    None,
-                    self.tr("Install Modules"),
-                    self.tr(
-                        """<p>The given code file <b>{0}</b> does not exist.</p>"""
-                    ).format(codeFile),
-                )
-
-    def __installModules(self, installs):
-        """
-        Private method to install the given list of modules.
-
-        @param installs list of module names to be installed
-        @type list of str
-        """
-        devicePath = self.__device.getWorkspace()
-
-        cpyVersion, board_id = circup.get_circuitpython_version(devicePath)
-        circup.CPY_VERSION = cpyVersion
-
-        with EricOverrideCursor():
-            availableModules = circup.get_bundle_versions(circup.get_bundles_list())
-            moduleNames = {}
-            for module, metadata in availableModules.items():
-                moduleNames[module.replace(".py", "")] = metadata
-            toBeInstalled = circup.get_dependencies(installs, mod_names=moduleNames)
-            deviceModules = circup.get_device_versions(devicePath)
-        if toBeInstalled is not None:
-            dependencies = [m for m in toBeInstalled if m not in installs]
-            ok = EricMessageBox.yesNo(
-                None,
-                self.tr("Install Modules"),
-                self.tr("""<p>Ready to install these modules?{0}{1}</p>""").format(
-                    """<ul><li>{0}</li></ul>""".format(
-                        "</li><li>".join(sorted(installs))
-                    ),
-                    self.tr("Dependencies:")
-                    + """<ul><li>{0}</li></ul>""".format(
-                        "</li><li>".join(sorted(dependencies))
-                    )
-                    if dependencies
-                    else "",
-                ),
-                yesDefault=True,
-            )
-            if ok:
-                installedModules = []
-                with EricOverrideCursor():
-                    for library in toBeInstalled:
-                        success = circup.install_module(
-                            devicePath,
-                            deviceModules,
-                            library,
-                            self.__installPyAct.isChecked(),
-                            moduleNames,
-                        )
-                        if success:
-                            installedModules.append(library)
-
-                if installedModules:
-                    EricMessageBox.information(
-                        None,
-                        self.tr("Install Modules"),
-                        self.tr(
-                            "<p>Installation complete. These modules were installed"
-                            " successfully.{0}</p>"
-                        ).format(
-                            """<ul><li>{0}</li></ul>""".format(
-                                "</li><li>".join(sorted(installedModules))
-                            ),
-                        ),
-                    )
-                else:
-                    EricMessageBox.information(
-                        None,
-                        self.tr("Install Modules"),
-                        self.tr(
-                            "<p>Installation complete. No modules were installed.</p>"
-                        ),
-                    )
-        else:
-            EricMessageBox.information(
-                None,
-                self.tr("Install Modules"),
-                self.tr("<p>No modules installation is required.</p>"),
-            )
-
-    @pyqtSlot()
-    def __uninstallModules(self):
-        """
-        Private slot to uninstall modules from the connected device.
-        """
-        devicePath = self.__device.getWorkspace()
-        libraryPath = os.path.join(devicePath, "lib")
-
-        with EricOverrideCursor():
-            deviceModules = circup.get_device_versions(devicePath)
-        modNames = {}
-        for moduleItem, metadata in deviceModules.items():
-            modNames[moduleItem.replace(".py", "").lower()] = metadata
-
-        dlg = EricListSelectionDialog(
-            sorted(modNames.keys()),
-            title=self.tr("Uninstall Modules"),
-            message=self.tr("Select the modules/packages to be uninstalled:"),
-            checkBoxSelection=True,
-        )
-        if dlg.exec() == QDialog.DialogCode.Accepted:
-            names = dlg.getSelection()
-            for name in names:
-                modulePath = modNames[name]["path"]
-                if os.path.isdir(modulePath):
-                    target = os.path.basename(os.path.dirname(modulePath))
-                    targetPath = os.path.join(libraryPath, target)
-                    # Remove the package directory.
-                    shutil.rmtree(targetPath)
-                else:
-                    target = os.path.basename(modulePath)
-                    targetPath = os.path.join(libraryPath, target)
-                    # Remove the module file
-                    os.remove(targetPath)
-
-            EricMessageBox.information(
-                None,
-                self.tr("Uninstall Modules"),
-                self.tr(
-                    """<p>These modules/packages were uninstalled from the connected"""
-                    """ device.{0}</p>"""
-                ).format("""<ul><li>{0}</li></ul>""".format("</li><li>".join(names))),
-            )
-
-    @pyqtSlot()
-    def __generateRequirements(self):
-        """
-        Private slot to generate requirements for the connected device.
-        """
-        from .RequirementsDialog import RequirementsDialog
-
-        devicePath = self.__device.getWorkspace()
-
-        cpyVersion, board_id = circup.get_circuitpython_version(devicePath)
-        circup.CPY_VERSION = cpyVersion
-
-        dlg = RequirementsDialog(devicePath=devicePath)
-        dlg.exec()
-
-
-def isCircupAvailable():
-    """
-    Function to check for the availability of 'circup'.
-
-    @return flag indicating the availability of 'circup'
-    @rtype bool
-    """
-    global circup
-
-    return circup is not None

eric ide

mercurial