eric7/Plugins/WizardPlugins/SetupWizard/SetupWizardDialog.py

Sun, 03 Jul 2022 13:52:59 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sun, 03 Jul 2022 13:52:59 +0200
branch
eric7
changeset 9202
81388c6065e8
parent 9201
2f1ccadee231
child 9205
b75da2ba2a1a
permissions
-rw-r--r--

Refined the setup wizard a little bit.

# -*- coding: utf-8 -*-

# Copyright (c) 2013 - 2022 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Module implementing the setup.py wizard dialog.
"""

import configparser
import datetime
import io
import os
import pathlib

import tomlkit

import trove_classifiers

from PyQt6.QtCore import pyqtSlot, Qt
from PyQt6.QtWidgets import (
    QDialog, QDialogButtonBox, QTreeWidgetItem, QListWidgetItem
)

from EricWidgets.EricApplication import ericApp
from EricWidgets import EricFileDialog
from EricWidgets.EricPathPicker import EricPathPickerModes

from .AddEntryPointDialog import AddEntryPointDialog
from .AddProjectUrlDialog import AddProjectUrlDialog
from .Ui_SetupWizardDialog import Ui_SetupWizardDialog

import Utilities
import Preferences


class SetupWizardDialog(QDialog, Ui_SetupWizardDialog):
    """
    Class implementing the setup.py wizard dialog.
    
    It displays a dialog for entering the parameters for the setup.py code
    generator.
    """
    def __init__(self, category, editor, parent=None):
        """
        Constructor
        
        @param category category of setup file to create
        @type str
        @param editor reference to the editor object to receive the code
        @type Editor
        @param parent reference to the parent widget (defaults to None)
        @type QWidget (optional)
        @exception ValueError raised for an illegal setup file category
        """
        if category not in ("setup.py", "setup.cfg", "pyproject.toml"):
            raise ValueError("illegal setup file category given")
        
        super().__init__(parent)
        self.setupUi(self)
        
        self.setWindowTitle(self.tr("{0} Wizard").format(category))
        
        self.__replies = []
        self.__category = category
        self.__editor = editor
        
        if category != "setup.py":
            self.introCheckBox.setVisible(False)
            self.importCheckBox.setVisible(False)
            self.metaDataCheckBox.setVisible(False)
        
        self.dataTabWidget.setCurrentIndex(0)
        
        self.packageRootPicker.setMode(EricPathPickerModes.DIRECTORY_MODE)
        self.sourceDirectoryPicker.setMode(EricPathPickerModes.DIRECTORY_MODE)
        
        self.__mandatoryStyleSheet = (
            "QLineEdit {border: 2px solid; border-color: #dd8888}"
            if ericApp().usesDarkPalette() else
            "QLineEdit {border: 2px solid; border-color: #800000}"
        )
        for lineEdit in [self.nameEdit, self.versionEdit,
                         self.homePageUrlEdit, self.authorEdit,
                         self.authorEmailEdit, self.maintainerEdit,
                         self.maintainerEmailEdit]:
            lineEdit.setStyleSheet(self.__mandatoryStyleSheet)
        
        self.__populateClassifiers()
        
        self.__okButton = self.buttonBox.button(
            QDialogButtonBox.StandardButton.Ok)
        self.__okButton.setEnabled(False)
        
        projectOpen = ericApp().getObject("Project").isOpen()
        self.projectButton.setEnabled(projectOpen)
        
        self.projectUrlsList.header().setSortIndicator(0, Qt.SortOrder.AscendingOrder)
        self.entryPointsList.header().setSortIndicator(0, Qt.SortOrder.AscendingOrder)
        
        self.descriptionContentTypeComboBox.addItem("", "")
        for contentType, mimetype in sorted([
            (self.tr("Plain Text"), "text/plain"),
            (self.tr("Markdown"), "text/markdown"),
            (self.tr("reStructuredText"), "text/x-rst")
            
        ]):
            self.descriptionContentTypeComboBox.addItem(contentType, mimetype)
        
        self.homePageUrlEdit.textChanged.connect(self.__enableOkButton)
        self.nameEdit.textChanged.connect(self.__enableOkButton)
        self.versionEdit.textChanged.connect(self.__enableOkButton)
        self.authorEdit.textChanged.connect(self.__enableOkButton)
        self.authorEmailEdit.textChanged.connect(self.__enableOkButton)
        self.maintainerEdit.textChanged.connect(self.__enableOkButton)
        self.maintainerEmailEdit.textChanged.connect(self.__enableOkButton)
    
    def __enableOkButton(self):
        """
        Private slot to set the state of the OK button.
        """
        enable = (
            bool(self.nameEdit.text()) and
            bool(self.versionEdit.text()) and
            bool(self.homePageUrlEdit.text()) and
            ((bool(self.authorEdit.text()) and
                bool(self.authorEmailEdit.text())) or
             (bool(self.maintainerEdit.text()) and
              bool(self.maintainerEmailEdit.text()))) and
            self.homePageUrlEdit.text().startswith(("http://", "https://"))
        )
        
        self.__okButton.setEnabled(enable)
    
    def __populateClassifiers(self):
        """
        Private method to populate the classifiers.
        """
        self.licenseClassifierComboBox.clear()
        self.classifiersList.clear()
        self.developmentStatusComboBox.clear()
        
        self.developmentStatusComboBox.addItem("", "")
        
        self.__classifiersDict = {}
        for classifier in trove_classifiers.sorted_classifiers:
            if classifier.startswith("License ::"):
                self.licenseClassifierComboBox.addItem(
                    "/".join(classifier.split(" :: ")[1:]),
                    classifier
                )
            elif classifier.startswith("Development Status ::"):
                self.developmentStatusComboBox.addItem(
                    classifier.split(" :: ")[1], classifier)
            else:
                self.__addClassifierEntry(classifier)
        self.__classifiersDict = {}
        
        self.licenseClassifierComboBox.setCurrentIndex(
            self.licenseClassifierComboBox.findText(
                "(GPLv3)",
                Qt.MatchFlag.MatchContains | Qt.MatchFlag.MatchCaseSensitive
            )
        )
    
    def __addClassifierEntry(self, classifier):
        """
        Private method to add a new entry to the list of trove classifiers.
        
        @param classifier classifier containing the data for the entry
        @type str
        """
        itm = None
        pitm = None
        dataList = classifier.split(" :: ")
        for index in range(len(dataList)):
            key = " :: ".join(dataList[:index + 1])
            if key not in self.__classifiersDict:
                if pitm is None:
                    itm = QTreeWidgetItem(
                        self.classifiersList, [dataList[index]])
                    pitm = itm
                else:
                    itm = QTreeWidgetItem(pitm, [dataList[index]])
                itm.setExpanded(True)
                self.__classifiersDict[key] = itm
            else:
                pitm = self.__classifiersDict[key]
        itm.setCheckState(0, Qt.CheckState.Unchecked)
        itm.setData(0, Qt.ItemDataRole.UserRole, classifier)
    
    def __getLicenseText(self):
        """
        Private method to get the license text.
        
        @return license text
        @rtype str
        """
        if not self.licenseClassifierCheckBox.isChecked():
            return self.licenseEdit.text()
        else:
            lic = self.licenseClassifierComboBox.currentText()
            if "(" in lic:
                lic = lic.rsplit("(", 1)[1].split(")", 1)[0]
            return lic
    
    def __getSetupPyCode(self, indLevel, indString):
        """
        Private method to get the source code for a 'setup.py' file.
        
        @param indLevel indentation level
        @type int
        @param indString string used for indentation (space or tab)
        @type str
        @return generated code
        @rtype str
        """
        # Note: all paths are created with '/'; setup will do the right thing
        
        # calculate our indentation level and the indentation string
        il = indLevel + 1
        istring = il * indString
        i1string = (il + 1) * indString
        i2string = (il + 2) * indString
        estring = os.linesep + indLevel * indString
        
        # now generate the code
        if self.introCheckBox.isChecked():
            sourceCode = "#!/usr/bin/env python3{0}".format(os.linesep)
            sourceCode += "# -*- coding: utf-8 -*-{0}{0}".format(os.linesep)
        else:
            sourceCode = ""
        
        if self.metaDataCheckBox.isChecked():
            sourceCode += '# metadata{0}'.format(os.linesep)
            sourceCode += '"{0}"{1}'.format(
                self.summaryEdit.text() or "Setup routine",
                os.linesep
            )
            sourceCode += '__version__ = "{0}"{1}'.format(
                self.versionEdit.text(), os.linesep)
            sourceCode += '__license__ = "{0}"{1}'.format(
                self.__getLicenseText(), os.linesep)
            sourceCode += '__author__ = "{0}"{1}'.format(
                self.authorEdit.text() or self.maintainerEdit.text(),
                os.linesep)
            sourceCode += '__email__ = "{0}"{1}'.format(
                self.authorEmailEdit.text() or self.maintainerEmailEdit.text(),
                os.linesep)
            sourceCode += '__url__ = "{0}"{1}'.format(
                self.homePageUrlEdit.text(), os.linesep)
            sourceCode += '__date__ = "{0}"{1}'.format(
                datetime.datetime.now().isoformat().split('.')[0], os.linesep)
            sourceCode += '__prj__ = "{0}"{1}'.format(
                self.nameEdit.text(), os.linesep)
            sourceCode += os.linesep
        
        if self.importCheckBox.isChecked():
            additionalImport = ", find_packages"
            sourceCode += "from setuptools import setup{0}{1}".format(
                additionalImport, os.linesep)
        if sourceCode:
            sourceCode += "{0}{0}".format(os.linesep)
        
        if self.descriptionFromFilesCheckBox.isChecked():
            sourceCode += 'def get_long_description():{0}'.format(os.linesep)
            sourceCode += '{0}descr = []{1}'.format(istring, os.linesep)
            sourceCode += '{0}for fname in ("{1}"):{2}'.format(
                istring,
                '", "'.join(self.descriptionEdit.toPlainText().splitlines()),
                os.linesep)
            sourceCode += (
                '{0}with open(fname, "r", encoding="utf-8") as f:{1}'
            ).format(i1string, os.linesep)
            sourceCode += '{0}descr.append(f.read()){1}'.format(
                i2string, os.linesep)
            sourceCode += '{0}return "\\n\\n".join(descr){1}'.format(
                istring, os.linesep)
            sourceCode += "{0}{0}".format(os.linesep)
        
        sourceCode += 'setup({0}'.format(os.linesep)
        sourceCode += '{0}name="{1}",{2}'.format(
            istring, self.nameEdit.text(), os.linesep)
        sourceCode += '{0}version="{1}",{2}'.format(
            istring, self.versionEdit.text(), os.linesep)
        
        if self.summaryEdit.text():
            sourceCode += '{0}description="{1}",{2}'.format(
                istring, self.summaryEdit.text(), os.linesep)
        
        if self.descriptionFromFilesCheckBox.isChecked():
            sourceCode += '{0}long_description=get_long_description(),{1}'.format(
                istring, os.linesep)
        elif self.descriptionEdit.toPlainText():
            sourceCode += '{0}long_description="""{1}""",{2}'.format(
                istring, self.descriptionEdit.toPlainText(), os.linesep)
        
        if self.descriptionContentTypeComboBox.currentData():
            sourceCode += '{0}long_description_content_type="{1}",{2}'.format(
                istring, self.descriptionContentTypeComboBox.currentData(), os.linesep)
        
        if self.authorEdit.text():
            sourceCode += '{0}author="{1}",{2}'.format(
                istring, self.authorEdit.text(), os.linesep)
            sourceCode += '{0}author_email="{1}",{2}'.format(
                istring, self.authorEmailEdit.text(), os.linesep)
        
        if self.maintainerEdit.text():
            sourceCode += '{0}maintainer="{1}",{2}'.format(
                istring, self.maintainerEdit.text(), os.linesep)
            sourceCode += '{0}maintainer_email="{1}",{2}'.format(
                istring, self.maintainerEmailEdit.text(), os.linesep)
        
        sourceCode += '{0}url="{1}",{2}'.format(
            istring, self.homePageUrlEdit.text(), os.linesep)
        if self.downloadUrlEdit.text():
            sourceCode += '{0}download_url="{1}",{2}'.format(
                istring, self.downloadUrlEdit.text(), os.linesep)
        
        if self.projectUrlsList.topLevelItemCount():
            sourceCode += '{0}project_urls={{{1}'.format(istring, os.linesep)
            for row in range(self.projectUrlsList.topLevelItemCount()):
                urlItem = self.projectUrlsList.topLevelItem(row)
                sourceCode += '{0}"{1}": "{2}",{3}'.format(
                    i1string, urlItem.text(0), urlItem.text(1), os.linesep)
            sourceCode += '{0}}},{1}'.format(istring, os.linesep)
        
        classifiers = []
        if not self.licenseClassifierCheckBox.isChecked():
            sourceCode += '{0}license="{1}",{2}'.format(
                istring, self.licenseEdit.text(), os.linesep)
        else:
            classifiers.append(
                self.licenseClassifierComboBox.itemData(
                    self.licenseClassifierComboBox.currentIndex()))
        
        platforms = self.platformsEdit.toPlainText().splitlines()
        if platforms:
            sourceCode += '{0}platforms=[{1}'.format(istring, os.linesep)
            sourceCode += '{0}"{1}"{2}'.format(
                i1string,
                '",{0}{1}"'.format(os.linesep, i1string).join(platforms),
                os.linesep)
            sourceCode += '{0}],{1}'.format(istring, os.linesep)
        
        if self.developmentStatusComboBox.currentIndex() != 0:
            classifiers.append(self.developmentStatusComboBox.currentData())
        
        itm = self.classifiersList.topLevelItem(0)
        while itm:
            itm.setExpanded(True)
            if itm.checkState(0) == Qt.CheckState.Checked:
                classifiers.append(itm.data(0, Qt.ItemDataRole.UserRole))
            itm = self.classifiersList.itemBelow(itm)
        
        # cleanup classifiers list - remove all invalid entries
        classifiers = [c for c in classifiers if bool(c)]
        if classifiers:
            sourceCode += '{0}classifiers=[{1}'.format(istring, os.linesep)
            sourceCode += '{0}"{1}"{2}'.format(
                i1string,
                '",{0}{1}"'.format(os.linesep, i1string).join(classifiers),
                os.linesep)
            sourceCode += '{0}],{1}'.format(istring, os.linesep)
        del classifiers
        
        if self.keywordsEdit.text():
            sourceCode += '{0}keywords="{1}",{2}'.format(
                istring, self.keywordsEdit.text(), os.linesep)
        
        if self.pyVersionEdit.text():
            sourceCode += '{0}python_requires="{1}",{2}'.format(
                istring, self.pyVersionEdit.text(), os.linesep)
        
        sourceCode += '{0}packages=find_packages('.format(istring)
        src = Utilities.fromNativeSeparators(
            self.sourceDirectoryPicker.text())
        excludePatterns = []
        for row in range(self.excludePatternList.count()):
            excludePatterns.append(
                self.excludePatternList.item(row).text())
        if src:
            sourceCode += '{0}{1}"{2}"'.format(os.linesep, i1string, src)
            if excludePatterns:
                sourceCode += ','
            else:
                sourceCode += '{0}{1}'.format(os.linesep, istring)
        if excludePatterns:
            sourceCode += '{0}{1}exclude=[{0}'.format(os.linesep, i1string)
            sourceCode += '{0}"{1}"{2}'.format(
                i2string,
                '",{0}{1}"'.format(os.linesep, i2string)
                .join(excludePatterns),
                os.linesep)
            sourceCode += '{0}]{1}{2}'.format(i1string, os.linesep, istring)
        sourceCode += '),{0}'.format(os.linesep)
        
        if self.includePackageDataCheckBox.isChecked():
            sourceCode += '{0}include_package_data = True,{1}'.format(
                istring, os.linesep)
        
        modules = []
        for row in range(self.modulesList.count()):
            modules.append(self.modulesList.item(row).text())
        if modules:
            sourceCode += '{0}py_modules=[{1}'.format(istring, os.linesep)
            sourceCode += '{0}"{1}"{2}'.format(
                i1string,
                '",{0}{1}"'.format(os.linesep, i1string).join(modules),
                os.linesep)
            sourceCode += '{0}],{1}'.format(istring, os.linesep)
        del modules
        
        if self.entryPointsList.topLevelItemCount():
            entryPoints = {
                "console_scripts": [],
                "gui_scripts": [],
            }
            for row in range(self.entryPointsList.topLevelItemCount()):
                itm = self.entryPointsList.topLevelItem(row)
                entryPoints[itm.data(0, Qt.ItemDataRole.UserRole)].append(
                    "{0} = {1}".format(itm.text(1), itm.text(2))
                )
            sourceCode += '{0}entry_points={{{1}'.format(istring, os.linesep)
            for epCategory in entryPoints:
                if entryPoints[epCategory]:
                    sourceCode += '{0}"{1}": [{2}'.format(
                        i1string, epCategory, os.linesep)
                    for entryPoint in entryPoints[epCategory]:
                        sourceCode += '{0}"{1}",{2}'.format(
                            i2string, entryPoint, os.linesep)
                    sourceCode += '{0}],{1}'.format(i1string, os.linesep)
            sourceCode += '{0}}},{1}'.format(istring, os.linesep)
        
        sourceCode += "){0}".format(estring)
        return sourceCode
    
    def __getSetupCfgCode(self):
        """
        Private method to get the source code for a 'setup.cfg' file.
        
        @return generated code
        @rtype str
        """
        from . import SetupCfgUtilities
        metadata = {
            "name": self.nameEdit.text(),
            "version": self.versionEdit.text(),
        }
        
        if self.summaryEdit.text():
            metadata["description"] = self.summaryEdit.text()
        
        if self.descriptionEdit.toPlainText():
            metadata["long_description"] = (
                "file: {0}".format(
                    ", ".join(self.descriptionEdit.toPlainText().splitlines())
                )
                if self.descriptionFromFilesCheckBox.isChecked() else
                self.descriptionEdit.toPlainText()
            )
        
        if self.descriptionContentTypeComboBox.currentData():
            metadata["long_description_content_type"] = (
                self.descriptionContentTypeComboBox.currentData()
            )
        
        if self.authorEdit.text():
            metadata["author"] = self.authorEdit.text()
            metadata["author_email"] = self.authorEmailEdit.text()
        
        if self.maintainerEdit.text():
            metadata["maintainer"] = self.maintainerEdit.text()
            metadata["maintainer_email"] = self.maintainerEmailEdit.text()
        
        metadata["url"] = self.homePageUrlEdit.text()
        if self.downloadUrlEdit.text():
            metadata["download_url"] = self.downloadUrlEdit.text()
        
        if self.projectUrlsList.topLevelItemCount():
            projectURLs = {}
            for row in range(self.projectUrlsList.topLevelItemCount()):
                urlItem = self.projectUrlsList.topLevelItem(row)
                projectURLs[urlItem.text(0)] = urlItem.text(1)
            metadata["project_urls"] = SetupCfgUtilities.toString(projectURLs)
        
        classifiers = []
        if not self.licenseClassifierCheckBox.isChecked():
            metadata["license"] = self.licenseEdit.text()
        else:
            classifiers.append(
                self.licenseClassifierComboBox.itemData(
                    self.licenseClassifierComboBox.currentIndex()))
        
        platforms = self.platformsEdit.toPlainText().splitlines()
        if platforms:
            metadata["platforms"] = SetupCfgUtilities.toString(platforms)
        
        if self.developmentStatusComboBox.currentIndex() != 0:
            classifiers.append(self.developmentStatusComboBox.currentData())
        
        itm = self.classifiersList.topLevelItem(0)
        while itm:
            itm.setExpanded(True)
            if itm.checkState(0) == Qt.CheckState.Checked:
                classifiers.append(itm.data(0, Qt.ItemDataRole.UserRole))
            itm = self.classifiersList.itemBelow(itm)
        
        # cleanup classifiers list - remove all invalid entries
        classifiers = [c for c in classifiers if bool(c)]
        if classifiers:
            metadata["classifiers"] = SetupCfgUtilities.toString(classifiers)
        
        if self.keywordsEdit.text():
            metadata["keywords"] = SetupCfgUtilities.toString(
                self.keywordsEdit.text().split())
        
        options = {
            "packages": "find:"
        }
        
        if self.pyVersionEdit.text():
            options["python_requires"] = self.pyVersionEdit.text()
        
        findOptions = {}
        src = Utilities.fromNativeSeparators(self.sourceDirectoryPicker.text())
        excludePatterns = []
        for row in range(self.excludePatternList.count()):
            excludePatterns.append(
                self.excludePatternList.item(row).text())
        if src:
            options["package_dir"] = SetupCfgUtilities.toString({"": src})
            findOptions["where"] = src
        if excludePatterns:
            findOptions["exclude"] = SetupCfgUtilities.toString(excludePatterns)
        
        if self.includePackageDataCheckBox.isChecked():
            options["include_package_data"] = SetupCfgUtilities.toString(True)
            packageData = {}    # placeholder section
        else:
            packageData = None
        
        modules = []
        for row in range(self.modulesList.count()):
            modules.append(self.modulesList.item(row).text())
        if modules:
            options["py_modules"] = SetupCfgUtilities.toString(modules)
        
        if self.entryPointsList.topLevelItemCount():
            entryPoints = {
                "console_scripts": {},
                "gui_scripts": {},
            }
            for row in range(self.entryPointsList.topLevelItemCount()):
                itm = self.entryPointsList.topLevelItem(row)
                entryPoints[itm.data(0, Qt.ItemDataRole.UserRole)][
                    itm.text(1)] = itm.text(2)
            for epType in list(entryPoints.keys()):
                if entryPoints[epType]:
                    entryPoints[epType] = SetupCfgUtilities.toString(
                        entryPoints[epType])
                else:
                    del entryPoints[epType]
        else:
            entryPoints = {}
        
        configDict = {
            "metadata": metadata,
            "options": options,
            "options.packages.find": findOptions,
        }
        if packageData is not None:
            configDict["options.package_data"] = packageData
        if entryPoints:
            configDict["options.entry_points"] = entryPoints
        
        cparser = configparser.ConfigParser()
        cparser.read_dict(configDict)
        sio = io.StringIO()
        cparser.write(sio)
        sourceCode = sio.getvalue()
        return sourceCode
    
    def __getPyprojectCode(self):
        """
        Private method to get the source code for a 'pyproject.toml' file.
        
        @return generated code
        @rtype str
        """
        doc = tomlkit.document()
        
        buildSystem = tomlkit.table()
        buildSystem["requires"] = ["setuptools>=61.0.0", "wheel"]
        buildSystem["build-backend"] = "setuptools.build_meta"
        doc["build-system"] = buildSystem
        
        project = tomlkit.table()
        project["name"] = self.nameEdit.text()
        project["version"] = self.versionEdit.text()
        
        if self.summaryEdit.text():
            project["description"] = self.summaryEdit.text()
        
        if self.descriptionEdit.toPlainText():
            if self.descriptionFromFilesCheckBox.isChecked():
                project["readme"] = self.descriptionEdit.toPlainText().splitlines()[0]
            else:
                readme = tomlkit.table()
                readme["text"] = self.descriptionEdit.toPlainText()
                readme["content-type"] = (
                    self.descriptionContentTypeComboBox.currentData()
                )
                project["readme"] = readme
        
        if self.authorEdit.text():
            authors = tomlkit.array()
            author = tomlkit.inline_table()
            author["name"] = self.authorEdit.text()
            author["email"] = self.authorEmailEdit.text()
            authors.append(author)
            project["authors"] = authors
        
        if self.maintainerEdit.text():
            maintainers = tomlkit.array()
            maintainer = tomlkit.inline_table()
            maintainer["name"] = self.maintainerEdit.text()
            maintainer["email"] = self.maintainerEmailEdit.text()
            maintainers.append(maintainer)
            project["maintainers"] = maintainers
        
        urls = tomlkit.table()
        urls["Homepage"] = self.homePageUrlEdit.text()
        if self.downloadUrlEdit.text():
            urls["Download"] = self.downloadUrlEdit.text()
        
        if self.projectUrlsList.topLevelItemCount():
            for row in range(self.projectUrlsList.topLevelItemCount()):
                urlItem = self.projectUrlsList.topLevelItem(row)
                urls[urlItem.text(0)] = urlItem.text(1)
        project["urls"] = urls
        
        classifiers = []
        if not self.licenseClassifierCheckBox.isChecked():
            license = tomlkit.table()
            license["text"] =  self.licenseEdit.text()
            project["license"] = license
        else:
            classifiers.append(
                self.licenseClassifierComboBox.itemData(
                    self.licenseClassifierComboBox.currentIndex()))
        
        if self.developmentStatusComboBox.currentIndex() != 0:
            classifiers.append(self.developmentStatusComboBox.currentData())
        
        itm = self.classifiersList.topLevelItem(0)
        while itm:
            itm.setExpanded(True)
            if itm.checkState(0) == Qt.CheckState.Checked:
                classifiers.append(itm.data(0, Qt.ItemDataRole.UserRole))
            itm = self.classifiersList.itemBelow(itm)
        
        # cleanup classifiers list - remove all invalid entries
        classifiers = [c for c in classifiers if bool(c)]
        if classifiers:
            classifiersArray = tomlkit.array()
            for classifier in classifiers:
                classifiersArray.add_line(classifier)
            classifiersArray.append(tomlkit.nl())
            project["classifiers"] = classifiersArray
        
        if self.keywordsEdit.text():
            keywords = tomlkit.array()
            for kw in self.keywordsEdit.text().split():
                keywords.add_line(kw)
            keywords.append(tomlkit.nl())
            project["keywords"] = keywords
        
        if self.pyVersionEdit.text():
            project["requires-python"] = self.pyVersionEdit.text()
        
        if self.entryPointsList.topLevelItemCount():
            entryPoints = {
                "console_scripts": {},
                "gui_scripts": {},
            }
            for row in range(self.entryPointsList.topLevelItemCount()):
                itm = self.entryPointsList.topLevelItem(row)
                entryPoints[itm.data(0, Qt.ItemDataRole.UserRole)][
                    itm.text(1)] = itm.text(2)
            
            if entryPoints["console_scripts"]:
                scripts = tomlkit.table()
                for name, function in entryPoints["console_scripts"].items():
                    scripts[name] = function
                project["scripts"] = scripts
            
            if entryPoints["gui_scripts"]:
                guiScripts = tomlkit.table()
                for name, function in entryPoints["gui_scripts"].items():
                    guiScripts[name] = function
                project["gui-scripts"] = guiScripts
        
        # placeholder
        dependencies = tomlkit.array()
        dependencies.append(tomlkit.comment(
            "TODO: enter project dependencies "                 # __NO-TASK__
        ))
        project["dependencies"] = dependencies
        
        doc["project"] = project
        
        setuptools = tomlkit.table()
        
        platforms = self.platformsEdit.toPlainText().splitlines()
        if platforms:
            platformsArray = tomlkit.array()
            for plt in platforms:
                platformsArray.add_line(plt)
            platformsArray.append(tomlkit.nl())
            setuptools["platforms"] = platformsArray
        
        setuptools["include-package-data"] = self.includePackageDataCheckBox.isChecked()
        if self.includePackageDataCheckBox.isChecked():
            # placeholder
            setuptools["package-data"] = tomlkit.table()
            setuptools["package-data"].add(tomlkit.comment(
                "TODO: enter package data patterns"             # __NO-TASK__
            ))
        
        if self.modulesList.count():
            modulesArray = tomlkit.array()
            for row in range(self.modulesList.count()):
                modulesArray.add_line(self.modulesList.item(row).text())
            modulesArray.append(tomlkit.nl())
            setuptools["py-modules"] = modulesArray
        
        findspec = tomlkit.table()
        src = Utilities.fromNativeSeparators(self.sourceDirectoryPicker.text())
        excludePatterns = []
        for row in range(self.excludePatternList.count()):
            excludePatterns.append(
                self.excludePatternList.item(row).text())
        if src:
            findspec["where"] = [ericApp().getObject("Project").getRelativePath(src)]
        if excludePatterns:
            excludePatternsArray = tomlkit.array()
            for pattern in excludePatterns:
                excludePatternsArray.add_line(pattern)
            excludePatternsArray.append(tomlkit.nl())
            findspec["exclude"] = excludePatternsArray
        
        if bool(findspec):
            setuptools["packages"] = tomlkit.table(is_super_table=True)
            setuptools["packages"]["find"] = findspec
        
        doc["tool"] = tomlkit.table(is_super_table=True)
        doc["tool"]["setuptools"] = setuptools
        
        sourceCode = tomlkit.dumps(doc)
        return sourceCode
    
    @pyqtSlot()
    def accept(self):
        """
        Public slot to handle pressing the OK button.
        """
        line, index = self.__editor.getCursorPosition()
        indLevel = self.__editor.indentation(line) // self.__editor.indentationWidth()
        if self.__editor.indentationsUseTabs():
            indString = '\t'
        else:
            indString = self.__editor.indentationWidth() * ' '
        
        if self.__category == "setup.py":
            sourceCode = self.__getSetupPyCode(indLevel, indString)
        elif self.__category == "setup.cfg":
            sourceCode = self.__getSetupCfgCode()
        elif self.__category == "pyproject.toml":
            sourceCode = self.__getPyprojectCode()
        else:
            # should not happen, but play it safe
            sourceCode = ""
        
        if sourceCode:
            line, index = self.__editor.getCursorPosition()
            # It should be done this way to allow undo
            self.__editor.beginUndoAction()
            self.__editor.insertAt(sourceCode, line, index)
            self.__editor.endUndoAction()
        
        super().accept()
    
    @pyqtSlot()
    def on_projectButton_clicked(self):
        """
        Private slot to populate some fields with data retrieved from the
        current project.
        """
        project = ericApp().getObject("Project")
        
        self.nameEdit.setText(project.getProjectName())
        try:
            self.versionEdit.setText(project.getProjectVersion())
            self.authorEdit.setText(project.getProjectAuthor())
            self.authorEmailEdit.setText(project.getProjectAuthorEmail())
            description = project.getProjectDescription()
        except AttributeError:
            self.versionEdit.setText(project.pdata["VERSION"][0])
            self.authorEdit.setText(project.pdata["AUTHOR"][0])
            self.authorEmailEdit.setText(project.pdata["EMAIL"][0])
            description = project.pdata["DESCRIPTION"][0]
        
        summary = (
            description.split(".", 1)[0].replace("\r", "").replace("\n", "") +
            "."
        )
        self.summaryEdit.setText(summary)
        self.descriptionEdit.setPlainText(description)
        
        self.packageRootPicker.setText(project.getProjectPath())
        
        # prevent overwriting of entries by disabling the button
        self.projectButton.setEnabled(False)
    
    def __getStartDir(self):
        """
        Private method to get the start directory for selection dialogs.
        
        @return start directory
        @rtype str
        """
        return (Preferences.getMultiProject("Workspace") or
                Utilities.getHomeDir())
    
    @pyqtSlot()
    def on_entryPointsList_itemSelectionChanged(self):
        """
        Private slot to handle a change of selected items of the
        entry points list.
        """
        self.deleteEntryPointButton.setEnabled(
            bool(self.entryPointsList.selectedItems()))
        self.editEntryPointButton.setEnabled(
            len(self.entryPointsList.selectedItems()) == 1)
    
    @pyqtSlot()
    def on_deleteEntryPointButton_clicked(self):
        """
        Private slot to delete the selected entry point items.
        """
        for itm in self.entryPointsList.selectedItems():
            self.entryPointsList.takeTopLevelItem(self.entryPointsList.row(itm))
            del itm
    
    @pyqtSlot()
    def on_addEntryPointButton_clicked(self):
        """
        Private slot to add an entry point to the list.
        """
        project = ericApp().getObject("Project")
        rootDir = (
            project.getProjectPath()
            if project.isOpen() else
            ""
        )
        dlg = AddEntryPointDialog(rootDir, parent=self)
        if dlg.exec() == QDialog.DialogCode.Accepted:
            epType, epCategory, name, script = dlg.getEntryPoint()
            itm = QTreeWidgetItem(self.entryPointsList, [epType, name, script])
            itm.setData(0, Qt.ItemDataRole.UserRole, epCategory)
    
    @pyqtSlot()
    def on_editEntryPointButton_clicked(self):
        """
        Private slot to edit the selected entry point.
        """
        project = ericApp().getObject("Project")
        rootDir = (
            project.getProjectPath()
            if project.isOpen() else
            ""
        )
        itm = self.entryPointsList.selectedItems()[0]
        dlg = AddEntryPointDialog(rootDir, epType=itm.text(0), name=itm.text(1),
                                  script=itm.text(2), parent=self)
        if dlg.exec() == QDialog.DialogCode.Accepted:
            epType, epCategory, name, script = dlg.getEntryPoint()
            itm.setText(0, epType)
            itm.setText(1, name)
            itm.setText(2, script)
            itm.setData(0, Qt.ItemDataRole.UserRole, epCategory)
    
    @pyqtSlot()
    def on_modulesList_itemSelectionChanged(self):
        """
        Private slot to handle a change of selected items of the
        modules list.
        """
        self.deleteModuleButton.setEnabled(
            bool(self.modulesList.selectedItems()))
    
    @pyqtSlot()
    def on_deleteModuleButton_clicked(self):
        """
        Private slot to delete the selected module items.
        """
        for itm in self.modulesList.selectedItems():
            self.modulesList.takeItem(self.modulesList.row(itm))
            del itm
    
    @pyqtSlot()
    def on_addModuleButton_clicked(self):
        """
        Private slot to add Python modules to the list.
        """
        startDir = self.packageRootPicker.text() or self.__getStartDir()
        modulesList = EricFileDialog.getOpenFileNames(
            self,
            self.tr("Add Python Modules"),
            startDir,
            self.tr("Python Files (*.py)"))
        for module in modulesList:
            module = module.replace(
                Utilities.toNativeSeparators(startDir), "")
            if module.startswith(("\\", "/")):
                module = module[1:]
            if module:
                QListWidgetItem(
                    str(pathlib.Path(module).with_suffix(""))
                    .replace("\\", ".")
                    .replace("/", "."),
                    self.modulesList
                )
    
    @pyqtSlot()
    def on_excludePatternList_itemSelectionChanged(self):
        """
        Private slot to handle a change of selected items of the
        exclude pattern list.
        """
        self.deleteExcludePatternButton.setEnabled(
            bool(self.excludePatternList.selectedItems()))
    
    @pyqtSlot()
    def on_deleteExcludePatternButton_clicked(self):
        """
        Private slot to delete the selected exclude pattern items.
        """
        for itm in self.excludePatternList.selectedItems():
            self.excludePatternList.takeItem(
                self.excludePatternList.row(itm))
            del itm
    
    @pyqtSlot()
    def on_addExludePatternButton_clicked(self):
        """
        Private slot to add an exclude pattern to the list.
        """
        pattern = (
            self.excludePatternEdit.text().replace("\\", ".").replace("/", ".")
        )
        if not self.excludePatternList.findItems(
            pattern,
            Qt.MatchFlag.MatchExactly | Qt.MatchFlag.MatchCaseSensitive
        ):
            QListWidgetItem(pattern, self.excludePatternList)
    
    @pyqtSlot(str)
    def on_excludePatternEdit_textChanged(self, txt):
        """
        Private slot to handle a change of the exclude pattern text.
        
        @param txt text of the line edit
        @type str
        """
        self.addExludePatternButton.setEnabled(bool(txt))
    
    @pyqtSlot()
    def on_excludePatternEdit_returnPressed(self):
        """
        Private slot handling a press of the return button of the
        exclude pattern edit.
        """
        self.on_addExludePatternButton_clicked()
    
    @pyqtSlot()
    def on_urlDeleteButton_clicked(self):
        """
        Private slot to delete the selected URL items.
        """
        for itm in self.projectUrlsList.selectedItems():
            self.projectUrlsList.takeTopLevelItem(self.projectUrlsList.row(itm))
            del itm
    
    @pyqtSlot()
    def on_urlAddButton_clicked(self):
        """
        Private slot to add a project URL to the list.
        """
        dlg = AddProjectUrlDialog(parent=self)
        if dlg.exec() == QDialog.DialogCode.Accepted:
            name, url = dlg.getUrl()
            QTreeWidgetItem(self.projectUrlsList, [name, url])
    
    @pyqtSlot()
    def on_urlEditButton_clicked(self):
        """
        Private slot to edit the selected project URL.
        """
        itm = self.projectUrlsList.selectedItems()[0]
        dlg = AddProjectUrlDialog(name=itm.text(0), url=itm.text(1), parent=self)
        if dlg.exec() == QDialog.DialogCode.Accepted:
            name, url = dlg.getUrl()
            itm.setText(0, name)
            itm.setText(1, url)
    
    @pyqtSlot()
    def on_projectUrlsList_itemSelectionChanged(self):
        """
        Private slot to handle a change of selected items of the
        project URLs list.
        """
        self.urlDeleteButton.setEnabled(bool(self.projectUrlsList.selectedItems()))
        self.urlEditButton.setEnabled(len(self.projectUrlsList.selectedItems()) == 1)

eric ide

mercurial