From f6341dc7eb4a3f956a1ce983d124200f0152cf8a Mon Sep 17 00:00:00 2001 From: Scriptbash <98601298+Scriptbash@users.noreply.github.com> Date: Wed, 1 Oct 2025 15:00:56 -0400 Subject: [PATCH 1/3] Migrate to pyQt6 --- plugin_exporter/__init__.py | 1 + plugin_exporter/help/source/conf.py | 112 ++++----- plugin_exporter/metadata.txt | 12 +- plugin_exporter/plugin_exporter.py | 227 +++++++++++------- plugin_exporter/plugin_exporter_dialog.py | 5 +- plugin_exporter/plugin_upload.py | 59 +++-- plugin_exporter/resources.py | 15 +- plugin_exporter/test/__init__.py | 2 +- plugin_exporter/test/qgis_interface.py | 32 +-- plugin_exporter/test/test_init.py | 46 ++-- .../test/test_plugin_exporter_dialog.py | 17 +- plugin_exporter/test/test_qgis_environment.py | 35 ++- plugin_exporter/test/test_resources.py | 13 +- plugin_exporter/test/test_translations.py | 23 +- plugin_exporter/test/utilities.py | 12 +- 15 files changed, 349 insertions(+), 262 deletions(-) diff --git a/plugin_exporter/__init__.py b/plugin_exporter/__init__.py index 15dc4f9..eaafce6 100644 --- a/plugin_exporter/__init__.py +++ b/plugin_exporter/__init__.py @@ -33,4 +33,5 @@ def classFactory(iface): # pylint: disable=invalid-name """ # from .plugin_exporter import PluginExporter + return PluginExporter(iface) diff --git a/plugin_exporter/help/source/conf.py b/plugin_exporter/help/source/conf.py index 1abc5ea..c0e369d 100644 --- a/plugin_exporter/help/source/conf.py +++ b/plugin_exporter/help/source/conf.py @@ -16,194 +16,199 @@ # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +# sys.path.insert(0, os.path.abspath('.')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.todo', 'sphinx.ext.imgmath', 'sphinx.ext.viewcode'] +extensions = ["sphinx.ext.todo", "sphinx.ext.imgmath", "sphinx.ext.viewcode"] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'PluginExporter' -copyright = u'2013, Francis Lapointe' +project = "PluginExporter" +copyright = "2013, Francis Lapointe" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '0.1' +version = "0.1" # The full version, including alpha/beta/rc tags. -release = '0.1' +release = "0.1" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_TemplateModuleNames = True +# add_TemplateModuleNames = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +html_theme = "default" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'TemplateClassdoc' +htmlhelp_basename = "TemplateClassdoc" # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). -#latex_paper_size = 'letter' +# latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). -#latex_font_size = '10pt' +# latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'PluginExporter.tex', u'PluginExporter Documentation', - u'Francis Lapointe', 'manual'), + ( + "index", + "PluginExporter.tex", + "PluginExporter Documentation", + "Francis Lapointe", + "manual", + ), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Additional stuff for the LaTeX preamble. -#latex_preamble = '' +# latex_preamble = '' # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output -------------------------------------------- @@ -211,6 +216,5 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'TemplateClass', u'PluginExporter Documentation', - [u'Francis Lapointe'], 1) + ("index", "TemplateClass", "PluginExporter Documentation", ["Francis Lapointe"], 1) ] diff --git a/plugin_exporter/metadata.txt b/plugin_exporter/metadata.txt index 62de2d9..fbe763c 100644 --- a/plugin_exporter/metadata.txt +++ b/plugin_exporter/metadata.txt @@ -6,7 +6,7 @@ name=Plugin Exporter qgisMinimumVersion=3.20 description=A QGIS plugin for exporting plugins -version=0.2.1 +version=0.2.2 author=Francis Lapointe email=francis.lapointe5@usherbrooke.ca @@ -16,11 +16,15 @@ tracker=https://github.com/Scriptbash/PluginExporter/issues repository=https://github.com/Scriptbash/PluginExporter # End of mandatory metadata +supportsQt6=True + # Recommended items: hasProcessingProvider=no # Uncomment the following line and add your changelog: changelog= + v0.2.2 + - Migrate to pyQt6 v0.2.1 - Fix import file filters v0.2.0 @@ -29,7 +33,7 @@ changelog= - Only the active plugins filter is now enabled by default # Tags are comma separated with spaces allowed -tags=python, qgis, plugin, importer, exporter +tags=python, qgis, plugin, importer, exporter, backup homepage=https://github.com/Scriptbash/PluginExporter category=Plugins @@ -40,10 +44,6 @@ experimental=False # deprecated flag (applies to the whole plugin, not just a single version) deprecated=False -# Since QGIS 3.8, a comma separated list of plugins to be installed -# (or upgraded) can be specified. -# Check the documentation for more information. -# plugin_dependencies= Category of the plugin: Raster, Vector, Database or Web # category= diff --git a/plugin_exporter/plugin_exporter.py b/plugin_exporter/plugin_exporter.py index 0a20c9c..3a9e337 100644 --- a/plugin_exporter/plugin_exporter.py +++ b/plugin_exporter/plugin_exporter.py @@ -21,6 +21,7 @@ * * ***************************************************************************/ """ + from qgis.PyQt.QtCore import QSettings, QTranslator, QCoreApplication from qgis.PyQt.QtGui import QIcon from qgis.PyQt.QtWidgets import QAction, QLabel, QCheckBox @@ -32,8 +33,10 @@ import csv import json import pathlib + # Initialize Qt resources from file resources.py from .resources import * + # Import the code for the dialog from .plugin_exporter_dialog import PluginExporterDialog @@ -54,11 +57,10 @@ def __init__(self, iface): # initialize plugin directory self.plugin_dir = os.path.dirname(__file__) # initialize locale - locale = QSettings().value('locale/userLocale')[0:2] + locale = QSettings().value("locale/userLocale")[0:2] locale_path = os.path.join( - self.plugin_dir, - 'i18n', - 'PluginExporter_{}.qm'.format(locale)) + self.plugin_dir, "i18n", "PluginExporter_{}.qm".format(locale) + ) if os.path.exists(locale_path): self.translator = QTranslator() @@ -67,7 +69,7 @@ def __init__(self, iface): # Declare instance attributes self.actions = [] - self.menu = self.tr(u'&Plugin Exporter') + self.menu = self.tr("&Plugin Exporter") # Check if plugin was started the first time in current QGIS session # Must be set in initGui() to survive plugin reloads @@ -89,7 +91,7 @@ def tr(self, message): :rtype: QString """ # noinspection PyTypeChecker,PyArgumentList,PyCallByClass - return QCoreApplication.translate('PluginExporter', message) + return QCoreApplication.translate("PluginExporter", message) def add_action( self, @@ -101,7 +103,8 @@ def add_action( add_to_toolbar=True, status_tip=None, whats_this=None, - parent=None): + parent=None, + ): """Add a toolbar icon to the toolbar. :param icon_path: Path to the icon for this action. Can be a resource @@ -157,9 +160,7 @@ def add_action( self.iface.addToolBarIcon(action) if add_to_menu: - self.iface.addPluginToMenu( - self.menu, - action) + self.iface.addPluginToMenu(self.menu, action) self.actions.append(action) @@ -168,12 +169,13 @@ def add_action( def initGui(self): """Create the menu entries and toolbar icons inside the QGIS GUI.""" - icon_path = ':/plugins/plugin_exporter/icon.png' + icon_path = ":/plugins/plugin_exporter/icon.png" self.add_action( icon_path, - text=self.tr(u'Export plugins'), + text=self.tr("Export plugins"), callback=self.run, - parent=self.iface.mainWindow()) + parent=self.iface.mainWindow(), + ) # will be set False in run() self.first_start = True @@ -181,9 +183,7 @@ def initGui(self): def unload(self): """Removes the plugin menu item and icon from QGIS GUI.""" for action in self.actions: - self.iface.removePluginMenu( - self.tr(u'&Plugin Exporter'), - action) + self.iface.removePluginMenu(self.tr("&Plugin Exporter"), action) self.iface.removeToolBarIcon(action) def run(self): @@ -194,8 +194,14 @@ def run(self): if self.first_start: self.first_start = False self.dlg = PluginExporterDialog() - self.core_plugins = ['processing', 'otbprovider', 'grassprovider', - 'db_manager', 'MetaSearch', 'sagaprovider'] + self.core_plugins = [ + "processing", + "otbprovider", + "grassprovider", + "db_manager", + "MetaSearch", + "sagaprovider", + ] self.pyplugin = pyplugin_installer.instance() self.pyplugin.reloadAndExportData() # Generate metadata cache self.get_plugins() @@ -211,7 +217,7 @@ def run(self): # show the dialog self.dlg.show() # Run the dialog event loop - result = self.dlg.exec_() + result = self.dlg.exec() # See if OK was pressed if result: if self.dlg.rd_export.isChecked(): @@ -226,7 +232,9 @@ def get_plugins(self): else: plugins = qgis.utils.available_plugins # All plugins if not self.dlg.chk_core_plugins.isChecked(): - plugins = [x for x in plugins if x not in self.core_plugins] # Exclude core plugins + plugins = [ + x for x in plugins if x not in self.core_plugins + ] # Exclude core plugins self.add_plugins_to_table(plugins) # Adds all the installed plugins into the table @@ -240,8 +248,10 @@ def add_plugins_to_table(self, plugins): if metadata is None: continue # Skip plugins with no metadata if self.dlg.chk_official_plugins.isChecked(): - if metadata['zip_repository'] == 'QGIS Official Plugin Repository': - self.plugins_metadata.append(metadata) # Adds the plugin metadata to the list + if metadata["zip_repository"] == "QGIS Official Plugin Repository": + self.plugins_metadata.append( + metadata + ) # Adds the plugin metadata to the list else: continue else: @@ -252,11 +262,11 @@ def add_plugins_to_table(self, plugins): chk_selected = QCheckBox() chk_selected.setChecked(True) lbl_plugin_name = QLabel() - lbl_plugin_name.setText(metadata['name']) + lbl_plugin_name.setText(metadata["name"]) lbl_version = QLabel() - lbl_version.setText(metadata['version_installed']) + lbl_version.setText(metadata["version_installed"]) lbl_author = QLabel() - lbl_author.setText(metadata['author_name']) + lbl_author.setText(metadata["author_name"]) table.setCellWidget(current_row, 0, chk_selected) table.setCellWidget(current_row, 1, lbl_plugin_name) @@ -306,60 +316,92 @@ def export_plugins(self): current_widget = table.cellWidget(row, col) if isinstance(current_widget, QCheckBox): if not current_widget.isChecked(): - break # Don't add plugins that are not checked + break # Don't add plugins that are not checked elif isinstance(current_widget, QLabel): current_plugin = next( - (item for item in self.plugins_metadata if item["name"] == current_widget.text()), None) + ( + item + for item in self.plugins_metadata + if item["name"] == current_widget.text() + ), + None, + ) if current_plugin: plugin_list.append(current_plugin) if plugin_list: if output_file: try: - if file_format == '.csv': - with open(output_file, 'w', encoding='utf8', newline='') as f: + if file_format == ".csv": + with open(output_file, "w", encoding="utf8", newline="") as f: keys = plugin_list[0].keys() dict_writer = csv.DictWriter(f, keys) dict_writer.writeheader() if repos: for key, value in repos.items(): - if key == 'QGIS Official Plugin Repository': + if key == "QGIS Official Plugin Repository": pass else: - dict_writer.writerow({'id': '-', 'name': key, 'zip_repository': value['url']}) + dict_writer.writerow( + { + "id": "-", + "name": key, + "zip_repository": value["url"], + } + ) dict_writer.writerows(plugin_list) - self.iface.messageBar().pushSuccess("Success", "Selected plugins were exported successfully.") - elif file_format == '.json': - with open(output_file, 'w') as file: + self.iface.messageBar().pushSuccess( + "Success", "Selected plugins were exported successfully." + ) + elif file_format == ".json": + with open(output_file, "w") as file: if repos: for key, value in repos.items(): - if key == 'QGIS Official Plugin Repository': + if key == "QGIS Official Plugin Repository": pass else: - plugin_list.insert(0, {'id': '-', 'name': key, 'zip_repository': value['url']}) + plugin_list.insert( + 0, + { + "id": "-", + "name": key, + "zip_repository": value["url"], + }, + ) json.dump(plugin_list, file) - self.iface.messageBar().pushSuccess("Success", "Selected plugins were exported successfully.") + self.iface.messageBar().pushSuccess( + "Success", "Selected plugins were exported successfully." + ) except IsADirectoryError: - self.iface.messageBar().pushMessage("Error", - "You must select a file, not a directory.", - level=Qgis.Critical) + self.iface.messageBar().pushMessage( + "Error", + "You must select a file, not a directory.", + level=Qgis.MessageLevel.Critical, + ) except PermissionError: - self.iface.messageBar().pushMessage("Error", - "You don't have permission to write to this directory. Please " - "pick another location and try again.", - level=Qgis.Critical) + self.iface.messageBar().pushMessage( + "Error", + "You don't have permission to write to this directory. Please " + "pick another location and try again.", + level=Qgis.MessageLevel.Critical, + ) except FileNotFoundError: - self.iface.messageBar().pushMessage("Error", - "No such file or directory. " - "Please check the path is valid.", - level=Qgis.Critical) + self.iface.messageBar().pushMessage( + "Error", + "No such file or directory. Please check the path is valid.", + level=Qgis.MessageLevel.Critical, + ) else: - self.iface.messageBar().pushMessage("Error", - "You must select an output file.", - level=Qgis.Critical) + self.iface.messageBar().pushMessage( + "Error", + "You must select an output file.", + level=Qgis.MessageLevel.Critical, + ) else: - self.iface.messageBar().pushMessage("Error", - "At least one plugin must be selected.", - level=Qgis.Critical) + self.iface.messageBar().pushMessage( + "Error", + "At least one plugin must be selected.", + level=Qgis.MessageLevel.Critical, + ) # Installs the plugins read from a .csv or .json file def import_plugins(self): @@ -367,66 +409,81 @@ def import_plugins(self): file_extension = pathlib.Path(input_file).suffix installed_plugins = qgis.utils.available_plugins - if file_extension == '.csv': + if file_extension == ".csv": try: - with open(input_file, 'r') as f: + with open(input_file, "r") as f: dict_reader = csv.DictReader(f) plugins = list(dict_reader) except: - self.iface.messageBar().pushMessage("Error", - "Unable to read the CSV file.", - level=Qgis.Critical) + self.iface.messageBar().pushMessage( + "Error", + "Unable to read the CSV file.", + level=Qgis.MessageLevel.Critical, + ) return - elif file_extension == '.json': + elif file_extension == ".json": try: f = open(input_file) plugins = json.load(f) except: - self.iface.messageBar().pushMessage("Error", - "Unable to read the JSON file.", - level=Qgis.Critical) + self.iface.messageBar().pushMessage( + "Error", + "Unable to read the JSON file.", + level=Qgis.MessageLevel.Critical, + ) return else: - self.iface.messageBar().pushMessage("Error", - "Unsupported file type.", - level=Qgis.Critical) + self.iface.messageBar().pushMessage( + "Error", "Unsupported file type.", level=Qgis.MessageLevel.Critical + ) return for plugin in plugins: if self.dlg.chk_skip_installed.isChecked(): - if plugin['id'] in installed_plugins: - self.iface.messageBar().pushInfo("Info", - "Skipped " + plugin['name'] + " as it's already installed.") + if plugin["id"] in installed_plugins: + self.iface.messageBar().pushInfo( + "Info", + "Skipped " + plugin["name"] + " as it's already installed.", + ) continue try: - if plugin['id'] == '-': # It's a third party repository + if plugin["id"] == "-": # It's a third party repository self.add_repository(plugin) else: - self.pyplugin.installPlugin(plugin['id']) - self.iface.messageBar().pushSuccess("Success", plugin['name'] + " was installed successfully.") + self.pyplugin.installPlugin(plugin["id"]) + self.iface.messageBar().pushSuccess( + "Success", plugin["name"] + " was installed successfully." + ) except KeyError: - self.iface.messageBar().pushMessage("Error", - "Could not install " + plugin['name'] + ".", - level=Qgis.Critical) + self.iface.messageBar().pushMessage( + "Error", + "Could not install " + plugin["name"] + ".", + level=Qgis.MessageLevel.Critical, + ) # This method is pretty much a copy of the addRepository function in # https://github.com/qgis/QGIS/blob/master/python/pyplugin_installer/installer.py def add_repository(self, repo_info): settings = QgsSettings() settings.beginGroup("app/plugin_repositories") - repo_name = repo_info['name'] - repo_url = repo_info['zip_repository'] + repo_name = repo_info["name"] + repo_url = repo_info["zip_repository"] if repo_name in repositories.all(): - self.iface.messageBar().pushInfo("Info", - "Found a repository with the same name. Skipping repository " + repo_name + - ". This could prevent a plugin from being installed.") + self.iface.messageBar().pushInfo( + "Info", + "Found a repository with the same name. Skipping repository " + + repo_name + + ". This could prevent a plugin from being installed.", + ) else: # Adds the repo inside the settings settings.setValue(repo_name + "/url", repo_url) settings.setValue(repo_name + "/authcfg", "") settings.setValue(repo_name + "/enabled", "True") self.pyplugin.reloadAndExportData() - self.iface.messageBar().pushSuccess("Success", repo_name + " was added to the repositories.") + self.iface.messageBar().pushSuccess( + "Success", repo_name + " was added to the repositories." + ) # Disables and enables widgets def toggle_widget(self): @@ -445,7 +502,7 @@ def toggle_widget(self): # Sets the file extension filter for the QgsFileWidget def set_filter(self): - if self.dlg.combo_file_format.currentText() == '.json': - self.dlg.file_output_export.setFilter('*.json') - elif self.dlg.combo_file_format.currentText() == '.csv': - self.dlg.file_output_export.setFilter('*.csv') + if self.dlg.combo_file_format.currentText() == ".json": + self.dlg.file_output_export.setFilter("*.json") + elif self.dlg.combo_file_format.currentText() == ".csv": + self.dlg.file_output_export.setFilter("*.csv") diff --git a/plugin_exporter/plugin_exporter_dialog.py b/plugin_exporter/plugin_exporter_dialog.py index 919037f..277449d 100644 --- a/plugin_exporter/plugin_exporter_dialog.py +++ b/plugin_exporter/plugin_exporter_dialog.py @@ -28,8 +28,9 @@ from qgis.PyQt import QtWidgets # This loads your .ui file so that PyQt can populate your plugin with the elements from Qt Designer -FORM_CLASS, _ = uic.loadUiType(os.path.join( - os.path.dirname(__file__), 'plugin_exporter_dialog_base.ui')) +FORM_CLASS, _ = uic.loadUiType( + os.path.join(os.path.dirname(__file__), "plugin_exporter_dialog_base.ui") +) class PluginExporterDialog(QtWidgets.QDialog, FORM_CLASS): diff --git a/plugin_exporter/plugin_upload.py b/plugin_exporter/plugin_upload.py index a88ea2b..244586d 100755 --- a/plugin_exporter/plugin_upload.py +++ b/plugin_exporter/plugin_upload.py @@ -1,8 +1,8 @@ #!/usr/bin/env python # coding=utf-8 """This script uploads a plugin package to the plugin repository. - Authors: A. Pasotti, V. Picavet - git sha : $TemplateVCSFormat +Authors: A. Pasotti, V. Picavet +git sha : $TemplateVCSFormat """ import sys @@ -13,10 +13,10 @@ standard_library.install_aliases() # Configuration -PROTOCOL = 'https' -SERVER = 'plugins.qgis.org' -PORT = '443' -ENDPOINT = '/plugins/RPC2/' +PROTOCOL = "https" +SERVER = "plugins.qgis.org" +PORT = "443" +ENDPOINT = "/plugins/RPC2/" VERBOSE = False @@ -32,15 +32,17 @@ def main(parameters, arguments): password=parameters.password, server=parameters.server, port=parameters.port, - endpoint=ENDPOINT) + endpoint=ENDPOINT, + ) print("Connecting to: %s" % hide_password(address)) server = xmlrpc.client.ServerProxy(address, verbose=VERBOSE) try: - with open(arguments[0], 'rb') as handle: + with open(arguments[0], "rb") as handle: plugin_id, version_id = server.plugin.upload( - xmlrpc.client.Binary(handle.read())) + xmlrpc.client.Binary(handle.read()) + ) print("Plugin ID: %s" % plugin_id) print("Version ID: %s" % version_id) except xmlrpc.client.ProtocolError as err: @@ -64,28 +66,41 @@ def hide_password(url, start=6): :param start: Position of start of password. :type start: int """ - start_position = url.find(':', start) + 1 - end_position = url.find('@') + start_position = url.find(":", start) + 1 + end_position = url.find("@") return "%s%s%s" % ( url[:start_position], - '*' * (end_position - start_position), - url[end_position:]) + "*" * (end_position - start_position), + url[end_position:], + ) if __name__ == "__main__": parser = OptionParser(usage="%prog [options] plugin.zip") parser.add_option( - "-w", "--password", dest="password", - help="Password for plugin site", metavar="******") + "-w", + "--password", + dest="password", + help="Password for plugin site", + metavar="******", + ) parser.add_option( - "-u", "--username", dest="username", - help="Username of plugin site", metavar="user") + "-u", + "--username", + dest="username", + help="Username of plugin site", + metavar="user", + ) parser.add_option( - "-p", "--port", dest="port", - help="Server port to connect to", metavar="80") + "-p", "--port", dest="port", help="Server port to connect to", metavar="80" + ) parser.add_option( - "-s", "--server", dest="server", - help="Specify server name", metavar="plugins.qgis.org") + "-s", + "--server", + dest="server", + help="Specify server name", + metavar="plugins.qgis.org", + ) options, args = parser.parse_args() if len(args) != 1: print("Please specify zip file.\n") @@ -98,7 +113,7 @@ def hide_password(url, start=6): if not options.username: # interactive mode username = getpass.getuser() - print("Please enter user name [%s] :" % username, end=' ') + print("Please enter user name [%s] :" % username, end=" ") res = input() if res != "": diff --git a/plugin_exporter/resources.py b/plugin_exporter/resources.py index 476b321..0b3d346 100644 --- a/plugin_exporter/resources.py +++ b/plugin_exporter/resources.py @@ -6,7 +6,7 @@ # # WARNING! All changes made in this file will be lost! -from PyQt5 import QtCore +from PyQt6 import QtCore qt_resource_data = b"\ \x00\x00\x3f\x21\ @@ -1057,7 +1057,7 @@ \x00\x00\x01\x89\xd5\x79\xf7\xe7\ " -qt_version = [int(v) for v in QtCore.qVersion().split('.')] +qt_version = [int(v) for v in QtCore.qVersion().split(".")] if qt_version < [5, 8, 0]: rcc_version = 1 qt_resource_struct = qt_resource_struct_v1 @@ -1065,10 +1065,17 @@ rcc_version = 2 qt_resource_struct = qt_resource_struct_v2 + def qInitResources(): - QtCore.qRegisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) + QtCore.qRegisterResourceData( + rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data + ) + def qCleanupResources(): - QtCore.qUnregisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) + QtCore.qUnregisterResourceData( + rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data + ) + qInitResources() diff --git a/plugin_exporter/test/__init__.py b/plugin_exporter/test/__init__.py index 8feeb0b..2aea085 100644 --- a/plugin_exporter/test/__init__.py +++ b/plugin_exporter/test/__init__.py @@ -1,2 +1,2 @@ # import qgis libs so that ve set the correct sip api version -import qgis # pylint: disable=W0611 # NOQA \ No newline at end of file +import qgis # pylint: disable=W0611 # NOQA diff --git a/plugin_exporter/test/qgis_interface.py b/plugin_exporter/test/qgis_interface.py index a407052..007d90b 100644 --- a/plugin_exporter/test/qgis_interface.py +++ b/plugin_exporter/test/qgis_interface.py @@ -14,29 +14,31 @@ """ -__author__ = 'tim@linfiniti.com' -__revision__ = '$Format:%H$' -__date__ = '10/01/2011' +__author__ = "tim@linfiniti.com" +__revision__ = "$Format:%H$" +__date__ = "10/01/2011" __copyright__ = ( - 'Copyright (c) 2010 by Ivan Mincik, ivan.mincik@gista.sk and ' - 'Copyright (c) 2011 German Carrillo, geotux_tuxman@linuxmail.org' - 'Copyright (c) 2014 Tim Sutton, tim@linfiniti.com' + "Copyright (c) 2010 by Ivan Mincik, ivan.mincik@gista.sk and " + "Copyright (c) 2011 German Carrillo, geotux_tuxman@linuxmail.org" + "Copyright (c) 2014 Tim Sutton, tim@linfiniti.com" ) import logging from qgis.PyQt.QtCore import QObject, pyqtSlot, pyqtSignal from qgis.core import QgsMapLayerRegistry from qgis.gui import QgsMapCanvasLayer -LOGGER = logging.getLogger('QGIS') +LOGGER = logging.getLogger("QGIS") -#noinspection PyMethodMayBeStatic,PyPep8Naming + +# noinspection PyMethodMayBeStatic,PyPep8Naming class QgisInterface(QObject): """Class to expose QGIS objects and functions to plugins. This class is here for enabling us to run unit tests only, so most methods are simply stubs. """ + currentLayerChanged = pyqtSignal(QgsMapCanvasLayer) def __init__(self, canvas): @@ -47,7 +49,7 @@ def __init__(self, canvas): self.canvas = canvas # Set up slots so we can mimic the behaviour of QGIS when layers # are added. - LOGGER.debug('Initialising canvas...') + LOGGER.debug("Initialising canvas...") # noinspection PyArgumentList QgsMapLayerRegistry.instance().layersAdded.connect(self.addLayers) # noinspection PyArgumentList @@ -58,7 +60,7 @@ def __init__(self, canvas): # For processing module self.destCrs = None - @pyqtSlot('QStringList') + @pyqtSlot("QStringList") def addLayers(self, layers): """Handle layers being added to the registry so they show up in canvas. @@ -67,9 +69,9 @@ def addLayers(self, layers): .. note:: The QgsInterface api does not include this method, it is added here as a helper to facilitate testing. """ - #LOGGER.debug('addLayers called on qgis_interface') - #LOGGER.debug('Number of layers being added: %s' % len(layers)) - #LOGGER.debug('Layer Count Before: %s' % len(self.canvas.layers())) + # LOGGER.debug('addLayers called on qgis_interface') + # LOGGER.debug('Number of layers being added: %s' % len(layers)) + # LOGGER.debug('Layer Count Before: %s' % len(self.canvas.layers())) current_layers = self.canvas.layers() final_layers = [] for layer in current_layers: @@ -78,9 +80,9 @@ def addLayers(self, layers): final_layers.append(QgsMapCanvasLayer(layer)) self.canvas.setLayerSet(final_layers) - #LOGGER.debug('Layer Count After: %s' % len(self.canvas.layers())) + # LOGGER.debug('Layer Count After: %s' % len(self.canvas.layers())) - @pyqtSlot('QgsMapLayer') + @pyqtSlot("QgsMapLayer") def addLayer(self, layer): """Handle a layer being added to the registry so it shows up in canvas. diff --git a/plugin_exporter/test/test_init.py b/plugin_exporter/test/test_init.py index a11ca44..a9cc2f6 100644 --- a/plugin_exporter/test/test_init.py +++ b/plugin_exporter/test/test_init.py @@ -1,19 +1,19 @@ # coding=utf-8 """Tests QGIS plugin init.""" -__author__ = 'Tim Sutton ' -__revision__ = '$Format:%H$' -__date__ = '17/10/2010' +__author__ = "Tim Sutton " +__revision__ = "$Format:%H$" +__date__ = "17/10/2010" __license__ = "GPL" -__copyright__ = 'Copyright 2012, Australia Indonesia Facility for ' -__copyright__ += 'Disaster Reduction' +__copyright__ = "Copyright 2012, Australia Indonesia Facility for " +__copyright__ += "Disaster Reduction" import os import unittest import logging import configparser -LOGGER = logging.getLogger('QGIS') +LOGGER = logging.getLogger("QGIS") class TestInit(unittest.TestCase): @@ -35,30 +35,34 @@ def test_read_init(self): # plugins/validator.py required_metadata = [ - 'name', - 'description', - 'version', - 'qgisMinimumVersion', - 'email', - 'author'] - - file_path = os.path.abspath(os.path.join( - os.path.dirname(__file__), os.pardir, - 'metadata.txt')) + "name", + "description", + "version", + "qgisMinimumVersion", + "email", + "author", + ] + + file_path = os.path.abspath( + os.path.join(os.path.dirname(__file__), os.pardir, "metadata.txt") + ) LOGGER.info(file_path) metadata = [] parser = configparser.ConfigParser() parser.optionxform = str parser.read(file_path) message = 'Cannot find a section named "general" in %s' % file_path - assert parser.has_section('general'), message - metadata.extend(parser.items('general')) + assert parser.has_section("general"), message + metadata.extend(parser.items("general")) for expectation in required_metadata: - message = ('Cannot find metadata "%s" in metadata source (%s).' % ( - expectation, file_path)) + message = 'Cannot find metadata "%s" in metadata source (%s).' % ( + expectation, + file_path, + ) self.assertIn(expectation, dict(metadata), message) -if __name__ == '__main__': + +if __name__ == "__main__": unittest.main() diff --git a/plugin_exporter/test/test_plugin_exporter_dialog.py b/plugin_exporter/test/test_plugin_exporter_dialog.py index 77f6212..3578e8e 100644 --- a/plugin_exporter/test/test_plugin_exporter_dialog.py +++ b/plugin_exporter/test/test_plugin_exporter_dialog.py @@ -8,9 +8,9 @@ """ -__author__ = 'francis.lapointe5@usherbrooke.ca' -__date__ = '2023-08-03' -__copyright__ = 'Copyright 2023, Francis Lapointe' +__author__ = "francis.lapointe5@usherbrooke.ca" +__date__ = "2023-08-03" +__copyright__ = "Copyright 2023, Francis Lapointe" import unittest @@ -19,6 +19,7 @@ from plugin_exporter_dialog import PluginExporterDialog from utilities import get_qgis_app + QGIS_APP = get_qgis_app() @@ -36,20 +37,20 @@ def tearDown(self): def test_dialog_ok(self): """Test we can click OK.""" - button = self.dialog.button_box.button(QDialogButtonBox.Ok) + button = self.dialog.button_box.button(QDialogButtonBox.StandardButton.Ok) button.click() result = self.dialog.result() - self.assertEqual(result, QDialog.Accepted) + self.assertEqual(result, QDialog.DialogCode.Accepted) def test_dialog_cancel(self): """Test we can click cancel.""" - button = self.dialog.button_box.button(QDialogButtonBox.Cancel) + button = self.dialog.button_box.button(QDialogButtonBox.StandardButton.Cancel) button.click() result = self.dialog.result() - self.assertEqual(result, QDialog.Rejected) + self.assertEqual(result, QDialog.DialogCode.Rejected) + if __name__ == "__main__": suite = unittest.makeSuite(PluginExporterDialogTest) runner = unittest.TextTestRunner(verbosity=2) runner.run(suite) - diff --git a/plugin_exporter/test/test_qgis_environment.py b/plugin_exporter/test/test_qgis_environment.py index 1becb30..f9c14e3 100644 --- a/plugin_exporter/test/test_qgis_environment.py +++ b/plugin_exporter/test/test_qgis_environment.py @@ -8,19 +8,17 @@ (at your option) any later version. """ -__author__ = 'tim@linfiniti.com' -__date__ = '20/01/2011' -__copyright__ = ('Copyright 2012, Australia Indonesia Facility for ' - 'Disaster Reduction') + +__author__ = "tim@linfiniti.com" +__date__ = "20/01/2011" +__copyright__ = "Copyright 2012, Australia Indonesia Facility for Disaster Reduction" import os import unittest -from qgis.core import ( - QgsProviderRegistry, - QgsCoordinateReferenceSystem, - QgsRasterLayer) +from qgis.core import QgsProviderRegistry, QgsCoordinateReferenceSystem, QgsRasterLayer from .utilities import get_qgis_app + QGIS_APP = get_qgis_app() @@ -31,30 +29,31 @@ def test_qgis_environment(self): """QGIS environment has the expected providers""" r = QgsProviderRegistry.instance() - self.assertIn('gdal', r.providerList()) - self.assertIn('ogr', r.providerList()) - self.assertIn('postgres', r.providerList()) + self.assertIn("gdal", r.providerList()) + self.assertIn("ogr", r.providerList()) + self.assertIn("postgres", r.providerList()) def test_projection(self): - """Test that QGIS properly parses a wkt string. - """ + """Test that QGIS properly parses a wkt string.""" crs = QgsCoordinateReferenceSystem() wkt = ( 'GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",' 'SPHEROID["WGS_1984",6378137.0,298.257223563]],' 'PRIMEM["Greenwich",0.0],UNIT["Degree",' - '0.0174532925199433]]') + "0.0174532925199433]]" + ) crs.createFromWkt(wkt) auth_id = crs.authid() - expected_auth_id = 'EPSG:4326' + expected_auth_id = "EPSG:4326" self.assertEqual(auth_id, expected_auth_id) # now test for a loaded layer - path = os.path.join(os.path.dirname(__file__), 'tenbytenraster.asc') - title = 'TestRaster' + path = os.path.join(os.path.dirname(__file__), "tenbytenraster.asc") + title = "TestRaster" layer = QgsRasterLayer(path, title) auth_id = layer.crs().authid() self.assertEqual(auth_id, expected_auth_id) -if __name__ == '__main__': + +if __name__ == "__main__": unittest.main() diff --git a/plugin_exporter/test/test_resources.py b/plugin_exporter/test/test_resources.py index 02cd59e..c3a5ada 100644 --- a/plugin_exporter/test/test_resources.py +++ b/plugin_exporter/test/test_resources.py @@ -8,16 +8,15 @@ """ -__author__ = 'francis.lapointe5@usherbrooke.ca' -__date__ = '2023-08-03' -__copyright__ = 'Copyright 2023, Francis Lapointe' +__author__ = "francis.lapointe5@usherbrooke.ca" +__date__ = "2023-08-03" +__copyright__ = "Copyright 2023, Francis Lapointe" import unittest from qgis.PyQt.QtGui import QIcon - class PluginExporterDialogTest(unittest.TestCase): """Test rerources work.""" @@ -31,14 +30,12 @@ def tearDown(self): def test_icon_png(self): """Test we can click OK.""" - path = ':/plugins/PluginExporter/icon.png' + path = ":/plugins/PluginExporter/icon.png" icon = QIcon(path) self.assertFalse(icon.isNull()) + if __name__ == "__main__": suite = unittest.makeSuite(PluginExporterResourcesTest) runner = unittest.TextTestRunner(verbosity=2) runner.run(suite) - - - diff --git a/plugin_exporter/test/test_translations.py b/plugin_exporter/test/test_translations.py index 035dc62..cf06532 100644 --- a/plugin_exporter/test/test_translations.py +++ b/plugin_exporter/test/test_translations.py @@ -7,12 +7,12 @@ (at your option) any later version. """ + from .utilities import get_qgis_app -__author__ = 'ismailsunni@yahoo.co.id' -__date__ = '12/10/2011' -__copyright__ = ('Copyright 2012, Australia Indonesia Facility for ' - 'Disaster Reduction') +__author__ = "ismailsunni@yahoo.co.id" +__date__ = "12/10/2011" +__copyright__ = "Copyright 2012, Australia Indonesia Facility for Disaster Reduction" import unittest import os @@ -26,26 +26,25 @@ class SafeTranslationsTest(unittest.TestCase): def setUp(self): """Runs before each test.""" - if 'LANG' in iter(os.environ.keys()): - os.environ.__delitem__('LANG') + if "LANG" in iter(os.environ.keys()): + os.environ.__delitem__("LANG") def tearDown(self): """Runs after each test.""" - if 'LANG' in iter(os.environ.keys()): - os.environ.__delitem__('LANG') + if "LANG" in iter(os.environ.keys()): + os.environ.__delitem__("LANG") def test_qgis_translations(self): """Test that translations work.""" parent_path = os.path.join(__file__, os.path.pardir, os.path.pardir) dir_path = os.path.abspath(parent_path) - file_path = os.path.join( - dir_path, 'i18n', 'af.qm') + file_path = os.path.join(dir_path, "i18n", "af.qm") translator = QTranslator() translator.load(file_path) QCoreApplication.installTranslator(translator) - expected_message = 'Goeie more' - real_message = QCoreApplication.translate("@default", 'Good morning') + expected_message = "Goeie more" + real_message = QCoreApplication.translate("@default", "Good morning") self.assertEqual(real_message, expected_message) diff --git a/plugin_exporter/test/utilities.py b/plugin_exporter/test/utilities.py index be7ee3b..8d16556 100644 --- a/plugin_exporter/test/utilities.py +++ b/plugin_exporter/test/utilities.py @@ -5,7 +5,7 @@ import logging -LOGGER = logging.getLogger('QGIS') +LOGGER = logging.getLogger("QGIS") QGIS_APP = None # Static variable used to hold hand to running QGIS app CANVAS = None PARENT = None @@ -13,7 +13,7 @@ def get_qgis_app(): - """ Start one QGIS application to test against. + """Start one QGIS application to test against. :returns: Handle to QGIS app, canvas, iface and parent. If there are any errors the tuple members will be returned as None. @@ -34,7 +34,7 @@ def get_qgis_app(): if QGIS_APP is None: gui_flag = True # All test will run qgis in gui mode - #noinspection PyPep8Naming + # noinspection PyPep8Naming QGIS_APP = QgsApplication(sys.argv, gui_flag) # Make sure QGIS_PREFIX_PATH is set in your env if needed! QGIS_APP.initQgis() @@ -43,19 +43,19 @@ def get_qgis_app(): global PARENT # pylint: disable=W0603 if PARENT is None: - #noinspection PyPep8Naming + # noinspection PyPep8Naming PARENT = QtGui.QWidget() global CANVAS # pylint: disable=W0603 if CANVAS is None: - #noinspection PyPep8Naming + # noinspection PyPep8Naming CANVAS = QgsMapCanvas(PARENT) CANVAS.resize(QtCore.QSize(400, 400)) global IFACE # pylint: disable=W0603 if IFACE is None: # QgisInterface is a stub implementation of the QGIS plugin interface - #noinspection PyPep8Naming + # noinspection PyPep8Naming IFACE = QgisInterface(CANVAS) return QGIS_APP, CANVAS, IFACE, PARENT From a89659eb2d20cb3007d4c9186be17b9119597782 Mon Sep 17 00:00:00 2001 From: Scriptbash <98601298+Scriptbash@users.noreply.github.com> Date: Wed, 1 Oct 2025 15:12:03 -0400 Subject: [PATCH 2/3] add qgis max version --- plugin_exporter/metadata.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/plugin_exporter/metadata.txt b/plugin_exporter/metadata.txt index fbe763c..e9f8c3f 100644 --- a/plugin_exporter/metadata.txt +++ b/plugin_exporter/metadata.txt @@ -5,6 +5,7 @@ [general] name=Plugin Exporter qgisMinimumVersion=3.20 +qgisMaximumVersion=4.99 description=A QGIS plugin for exporting plugins version=0.2.2 author=Francis Lapointe From 3f03a51473b071cd801e2fac0804a962b4d41ff7 Mon Sep 17 00:00:00 2001 From: Scriptbash <98601298+Scriptbash@users.noreply.github.com> Date: Wed, 1 Oct 2025 15:14:19 -0400 Subject: [PATCH 3/3] remove duplicated metadata line --- plugin_exporter/metadata.txt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/plugin_exporter/metadata.txt b/plugin_exporter/metadata.txt index e9f8c3f..28beb2b 100644 --- a/plugin_exporter/metadata.txt +++ b/plugin_exporter/metadata.txt @@ -45,10 +45,6 @@ experimental=False # deprecated flag (applies to the whole plugin, not just a single version) deprecated=False - -Category of the plugin: Raster, Vector, Database or Web -# category= - # If the plugin can run on QGIS Server. server=False