diff -r 831e08e94960 -r 2f1ccadee231 eric7/Plugins/WizardPlugins/SetupWizard/SetupWizardDialog.py --- a/eric7/Plugins/WizardPlugins/SetupWizard/SetupWizardDialog.py Sat Jul 02 17:22:06 2022 +0200 +++ b/eric7/Plugins/WizardPlugins/SetupWizard/SetupWizardDialog.py Sat Jul 02 18:53:56 2022 +0200 @@ -7,8 +7,13 @@ Module implementing the setup.py wizard dialog. """ +import configparser +import datetime +import io import os -import datetime +import pathlib + +import tomlkit import trove_classifiers @@ -21,6 +26,8 @@ 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 @@ -34,17 +41,29 @@ It displays a dialog for entering the parameters for the setup.py code generator. """ - def __init__(self, parent=None): + def __init__(self, category, parent=None): """ Constructor - @param parent reference to the parent widget - @type QWidget + @param category category of setup file to create + @type str + @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.__replies = [] + self.__category = category + + if category != "setup.py": + self.introCheckBox.setVisible(False) + self.importCheckBox.setVisible(False) + self.metaDataCheckBox.setVisible(False) self.dataTabWidget.setCurrentIndex(0) @@ -71,6 +90,18 @@ 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) @@ -168,9 +199,9 @@ lic = lic.rsplit("(", 1)[1].split(")", 1)[0] return lic - def getCode(self, indLevel, indString): + def __getSetupPyCode(self, indLevel, indString): """ - Public method to get the source code. + Private method to get the source code for a 'setup.py' file. @param indLevel indentation level @type int @@ -229,14 +260,15 @@ 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( + sourceCode += '{0}for fname in ("{1})":{2}'.format( istring, '", "'.join(self.descriptionEdit.toPlainText().splitlines()), os.linesep) - sourceCode += '{0}{0}with open(fname) as f:{1}'.format( - istring, os.linesep) - sourceCode += '{0}{0}{0}descr.append(f.read()){1}'.format( - istring, 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) @@ -258,6 +290,10 @@ 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) @@ -272,6 +308,17 @@ 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(): @@ -292,9 +339,7 @@ sourceCode += '{0}],{1}'.format(istring, os.linesep) if self.developmentStatusComboBox.currentIndex() != 0: - classifiers.append( - self.developmentStatusComboBox.itemData( - self.developmentStatusComboBox.currentIndex())) + classifiers.append(self.developmentStatusComboBox.currentData()) itm = self.classifiersList.topLevelItem(0) while itm: @@ -318,6 +363,10 @@ 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()) @@ -357,21 +406,376 @@ sourceCode += '{0}],{1}'.format(istring, os.linesep) del modules - scripts = [] - for row in range(self.scriptsList.count()): - scripts.append(self.scriptsList.item(row).text()) - if scripts: - sourceCode += '{0}scripts=[{1}'.format(istring, os.linesep) - sourceCode += '{0}"{1}"{2}'.format( - i1string, - '",{0}{1}"'.format(os.linesep, i1string).join(scripts), - os.linesep) - sourceCode += '{0}],{1}'.format(istring, os.linesep) - del scripts + 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 + + def getCode(self, indLevel, indString): + """ + Public method to get the source code. + + @param indLevel indentation level + @type int + @param indString string used for indentation (space or tab) + @type str + @return generated code + @rtype str + """ + if self.__category == "setup.py": + return self.__getSetupPyCode(indLevel, indString) + elif self.__category == "setup.cfg": + return self.__getSetupCfgCode() + elif self.__category == "pyproject.toml": + return self.__getPyprojectCode() + else: + # should not happen, but play it safe + return "" + @pyqtSlot() def on_projectButton_clicked(self): """ @@ -415,43 +819,62 @@ Utilities.getHomeDir()) @pyqtSlot() - def on_scriptsList_itemSelectionChanged(self): + def on_entryPointsList_itemSelectionChanged(self): """ Private slot to handle a change of selected items of the - scripts list. + entry points list. """ - self.deleteScriptButton.setEnabled( - len(self.scriptsList.selectedItems()) > 0) + self.deleteEntryPointButton.setEnabled( + bool(self.entryPointsList.selectedItems())) + self.editEntryPointButton.setEnabled( + len(self.entryPointsList.selectedItems()) == 1) @pyqtSlot() - def on_deleteScriptButton_clicked(self): - """ - Private slot to delete the selected script items. + def on_deleteEntryPointButton_clicked(self): """ - for itm in self.scriptsList.selectedItems(): - self.scriptsList.takeItem( - self.scriptsList.row(itm)) + 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_addScriptButton_clicked(self): + def on_addEntryPointButton_clicked(self): """ - Private slot to add scripts to the list. + Private slot to add an entry point to the list. """ - startDir = self.packageRootPicker.text() or self.__getStartDir() - scriptsList = EricFileDialog.getOpenFileNames( - self, - self.tr("Add Scripts"), - startDir, - self.tr("Python Files (*.py);;All Files(*)")) - for script in scriptsList: - script = script.replace( - Utilities.toNativeSeparators(startDir), "") - if script.startswith(("\\", "/")): - script = script[1:] - if script: - QListWidgetItem(Utilities.fromNativeSeparators(script), - self.scriptsList) + 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): @@ -460,16 +883,15 @@ modules list. """ self.deleteModuleButton.setEnabled( - len(self.modulesList.selectedItems()) > 0) + bool(self.modulesList.selectedItems())) @pyqtSlot() def on_deleteModuleButton_clicked(self): """ - Private slot to delete the selected script items. + Private slot to delete the selected module items. """ for itm in self.modulesList.selectedItems(): - self.modulesList.takeItem( - self.modulesList.row(itm)) + self.modulesList.takeItem(self.modulesList.row(itm)) del itm @pyqtSlot() @@ -489,9 +911,12 @@ if module.startswith(("\\", "/")): module = module[1:] if module: - QListWidgetItem(os.path.splitext(module)[0] - .replace("\\", ".").replace("/", "."), - self.modulesList) + QListWidgetItem( + str(pathlib.Path(module).with_suffix("")) + .replace("\\", ".") + .replace("/", "."), + self.modulesList + ) @pyqtSlot() def on_excludePatternList_itemSelectionChanged(self): @@ -500,7 +925,7 @@ exclude pattern list. """ self.deleteExcludePatternButton.setEnabled( - len(self.excludePatternList.selectedItems()) > 0) + bool(self.excludePatternList.selectedItems())) @pyqtSlot() def on_deleteExcludePatternButton_clicked(self): @@ -543,3 +968,43 @@ 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)