diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d643c7d --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +__pycache__/ + +# Project specific files +plugin/idg/config/*.png +plugin/idg/config/*.jpg +plugin/idg/config/*.jpeg +plugin/idg/config/*.qgz +plugin/idg/config/*.qgs + +# PyCharm +.idea/ \ No newline at end of file diff --git a/README.md b/README.md index 3d0dd1c..fef271c 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,12 @@ Plugin pour QGIS 3 fournissant un accès simple aux données de l'ensemble des Infrastructure de Données Géographiques (IDG) et d'autres ressources nationales géographiques utiles. -Canal de discussions : https://matrix.to/#/!DqHgKIoltGIikFRreo:matrix.org +Canal de discussions : ![QGIS Browser](repo/screenshot_browser_1.png) Accès aux données des plateformes : + - [DataGrandEst](https://datagrandest.fr/) - [GéoBretagne](https://geobretagne.fr) - [Géo2France](https://geo2france.fr) @@ -17,12 +18,10 @@ Accès aux données des plateformes : Pré-requis : -* QGIS version LTR [3.28] ou supérieure -* Une connexion Internet - -* Installation depuis le dépot QGIS : dans le gestionnaire d'exentions (Extensions > Installer/Gérer les extensions), activer les extensions expérimentales et rechercher le plugin _IDG_ -* Installation depuis le fichier zip : télécharger depuis la derniere [release](https://github.com/geo2france/idg-qgis-plugin/releases) depuis le dépot github. - +- QGIS version LTR [3.28] ou supérieure +- Une connexion Internet +- Installation depuis le dépot QGIS : dans le gestionnaire d'exentions (Extensions > Installer/Gérer les extensions), activer les extensions expérimentales et rechercher le plugin _IDG_ +- Installation depuis le fichier zip : télécharger depuis la derniere [release](https://github.com/geo2france/idg-qgis-plugin/releases) depuis le dépot github. ## Utilisation @@ -32,27 +31,26 @@ Créer un nouveau projet et y ajouter les couches que vous souhaitez diffuser. > **Warning** > Les couches doivent pouvoir être accessibles depuis n'importe où (fichiers distants, flux WMS/WFS, etc.), il ne doit **pas** s'agir de fichiers locaux. - Il est recommandé d'[organiser les couches en groupes et sous-groupes](https://docs.qgis.org/3.22/fr/docs/user_manual/introduction/general_tools.html#group-layers-interact). Dans les propriétés du projets, remplir les champs suivants : - **Métadonnées > Identification > Titre** : Le nom de la plateforme qui sera visible par l'utilisateur (ex : Geo2France) - **Métadonnées > Identification > Résumé** : Facultatif, une brève présentation qui sera visible au survol -- **Métadonnées > Liens** : Vous pouvez ajouter ici des liens vers les différents services de votre plateforme (ex : contact, catalogue, etc.) +- **Métadonnées > Liens** : Vous pouvez ajouter ici des liens vers les différents services de votre plateforme (ex : contact, catalogue, etc.) Ceux-ci seront accessibles à l'utilisateur via un clic droit sur le nom de la plateforme. Ajoutez un lien nommé `icon` pour ajouter une icône personnalisée à la plateforme (png ou svg) Pour chaque couche, vous pouvez définir : + - **Métadonnées > Identification > Titre & Réumé** Un titre et un résumé - **Métadonnées > Identification > Liens** créer un lien nommé "Metadata" vers la fiche de métadonnées - Une symbologie (style, étiquettes, formulaires, etc.) Enregistrez le fichier projet (qgs ou qgz) et **déposez le sur un un serveur web** accessible depuis l'exterieur (serveur HTTP, Github, cloud, etc.). -Pour proposer l'ajout d'une plateforme dans le plugin : **éditez le fichier [default_idg.json](plugin/idg/config/default_idg.json)** +Pour proposer l'ajout d'une plateforme dans le plugin : **éditez le fichier [default_idg.json](plugin/idg/config/default_idg.json)** et faites une _pull request_. - ### Utilisateur Dans le panneau _navigateur_ sur la gauche, double-cliquez sur l'icone **IDG** : cela déroulera les différentes plateformes disponibles. @@ -63,11 +61,11 @@ Depuis les paramètres du plugin, vous avez la possibilité d'afficher/masquer l ### Auteurs -* Benjamin Chartier, Jean-Baptiste Desbas +- Benjamin Chartier, Jean-Baptiste Desbas ### Source d'inspiration -* Nicolas Damiens +- Nicolas Damiens ### Contributeurs @@ -75,9 +73,8 @@ Depuis les paramètres du plugin, vous avez la possibilité d'afficher/masquer l ### Autres remerciements -* [Julien Moura](https://github.com/Guts) (Oslandia) pour le [template](https://oslandia.gitlab.io/qgis/template-qgis-plugin/) du plugin. -* Auteurs des icônes de QGIS, reprises dans l'arbre des ressources - +- [Julien Moura](https://github.com/Guts) (Oslandia) pour le [template](https://oslandia.gitlab.io/qgis/template-qgis-plugin/) du plugin. +- Auteurs des icônes de QGIS, reprises dans l'arbre des ressources ## Licence diff --git a/plugin/idg/.gitignore b/plugin/idg/.gitignore deleted file mode 100644 index ba0430d..0000000 --- a/plugin/idg/.gitignore +++ /dev/null @@ -1 +0,0 @@ -__pycache__/ \ No newline at end of file diff --git a/plugin/idg/browser/__init__.py b/plugin/idg/browser/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plugin/idg/toolbelt/browser.py b/plugin/idg/browser/browser.py similarity index 74% rename from plugin/idg/toolbelt/browser.py rename to plugin/idg/browser/browser.py index 19d4b0b..2eb2033 100644 --- a/plugin/idg/toolbelt/browser.py +++ b/plugin/idg/browser/browser.py @@ -1,5 +1,6 @@ +import webbrowser + from qgis.core import ( - Qgis, QgsDataItemProvider, QgsDataCollectionItem, QgsDataItem, @@ -14,14 +15,14 @@ ) from qgis.gui import QgisInterface from qgis.PyQt.QtGui import QIcon -from qgis.PyQt.Qt import QWidget -from idg.toolbelt import PluginGlobals -from .remote_platforms import RemotePlatforms -from idg.__about__ import __title__ from qgis.PyQt.QtWidgets import QAction, QMenu from qgis.utils import iface -import os.path -import webbrowser + +from idg.plugin_globals import PluginGlobals +from idg.gui.actions import PluginActions +from idg.__about__ import DIR_PLUGIN_ROOT + +from idg.browser.remote_platforms import RemotePlatforms def find_catalog_url(metadata: QgsAbstractMetadataBase): @@ -42,65 +43,72 @@ def project_custom_icon_url(metadata: QgsAbstractMetadataBase): class IdgProvider(QgsDataItemProvider): def __init__(self, iface=iface): self.iface = iface + self.root = None QgsDataItemProvider.__init__(self) def name(self): - return "IDG Provider" + return PluginGlobals.BROWSER_PROVIDER_NAME def capabilities(self): return QgsDataProvider.Net def createDataItem(self, path, parentItem): - self.root = RootCollection(self.iface, parent = parentItem) + self.root = RootCollection(self.iface, parent=parentItem) return self.root class RootCollection(QgsDataCollectionItem): def __init__(self, iface: QgisInterface, parent): self.iface = iface - QgsDataCollectionItem.__init__(self, parent, "IDG", "/IDG") - self.setIcon( - QIcon( - PluginGlobals.instance().plugin_path - + "/resources/images/layers-svgrepo-com.svg" - ) - ) + plugin_tag = PluginGlobals.PLUGIN_TAG + QgsDataCollectionItem.__init__(self, parent, plugin_tag, f"/{plugin_tag}") + + plugin_path = DIR_PLUGIN_ROOT.resolve() + icon_path = plugin_path / "resources" / "images" / "layers-svgrepo-com.svg" + self.setIcon(QIcon(str(icon_path.resolve()))) def actions(self, parent): actions = list() - add_idg_action = QAction(QIcon(), self.tr("Settings..."), parent) - add_idg_action.triggered.connect( - lambda: self.iface.showOptionsDialog( - currentPage="mOptionsPage{}".format(__title__) - ) - ) - actions.append(add_idg_action) + + # Settings action + actions.append(PluginActions.action_show_settings) + + # Download and reload all files action + actions.append(PluginActions.action_reload_idgs) + return actions def menus(self, parent): - menu = QMenu(title=self.tr("Plateforms"), parent=parent) - menu.setEnabled(False) # dev - for pf, checked in zip( - ["DataGrandEst", "GeoBretagne", "Geo2France", "Indigeo"], - [True, False, True, False], - ): # pour maquette TODO boucler sur une variable de conf - action = QAction(pf, menu, checkable=True) - action.setChecked(checked) - menu.addAction( - action - ) # TODO l'action permet d'activer/désactiver une plateforme. La désactivation supprime le DataCollectionItem et désactive le download du fichier de conf - menu.addSeparator() - menu.addAction( - QAction( - self.tr("Add URL"), - menu, - ) - ) # TODO Liens vers le panneau Options de QGIS - return [menu] + # todo: reactivate this menu and make it operational + # menu = QMenu(title=self.tr("Plateforms"), parent=parent) + # menu.setEnabled(False) # dev + # for pf, checked in zip( + # ["DataGrandEst", "GeoBretagne", "Geo2France", "Indigeo"], + # [True, False, True, False], + # ): # pour maquette TODO boucler sur une variable de conf + # action = QAction(pf, menu, checkable=True) + # action.setChecked(checked) + # menu.addAction( + # action + # ) # TODO l'action permet d'activer/désactiver une plateforme. La désactivation supprime le DataCollectionItem et désactive le download du fichier de conf + # menu.addSeparator() + # menu.addAction( + # QAction( + # self.tr("Add URL…"), + # menu, + # ) + # ) + # return [menu] + + return list() def createChildren(self): children = [] - for pfc in [PlatformCollection(plateform=pf, parent=self) for pf in RemotePlatforms().plateforms if not pf.is_hidden()]: + for pfc in [ + PlatformCollection(plateform=pf, parent=self) + for pf in RemotePlatforms().plateforms + if not pf.is_hidden() + ]: children.append(pfc) return children @@ -168,7 +176,7 @@ def hide_plateform(pf): class GroupItem(QgsDataCollectionItem): def __init__(self, parent, name, group): - self.path = os.path.join(parent.path, group.name()) + self.path = parent.path + "/" + group.name() self.group = group QgsDataCollectionItem.__init__(self, parent, name, self.path) self.setIcon(QIcon(QgsApplication.iconPath("mIconFolder.svg"))) @@ -193,7 +201,7 @@ class LayerItem(QgsDataItem): def __init__(self, parent, name, layer): self.layer = layer self.catalog_url = find_catalog_url(layer.metadata()) - self.path = os.path.join(parent.path, layer.id()) + self.path = parent.path + "/" + layer.id() QgsDataItem.__init__(self, QgsDataItem.Custom, parent, name, self.path) self.setState(QgsDataItem.Populated) # no children self.setToolTip(self.layer.metadata().abstract()) @@ -227,13 +235,13 @@ def addLayer(self): QgsProject.instance().addMapLayer(self.layer) def actions(self, parent): - ac_open_meta = QAction(self.tr("Show metadata"), parent) + ac_open_meta = QAction(self.tr("Show metadata…"), parent) if self.catalog_url is not None: ac_open_meta.triggered.connect(self.openUrl) else: ac_open_meta.setEnabled(False) - ac_show_layer = QAction(self.tr("Display layer"), parent) + ac_show_layer = QAction(self.tr("Add layer to map"), parent) ac_show_layer.triggered.connect(self.addLayer) actions = [ diff --git a/plugin/idg/toolbelt/network_manager.py b/plugin/idg/browser/network_manager.py similarity index 100% rename from plugin/idg/toolbelt/network_manager.py rename to plugin/idg/browser/network_manager.py diff --git a/plugin/idg/toolbelt/remote_platforms.py b/plugin/idg/browser/remote_platforms.py similarity index 80% rename from plugin/idg/toolbelt/remote_platforms.py rename to plugin/idg/browser/remote_platforms.py index 189411b..5dd07b7 100644 --- a/plugin/idg/toolbelt/remote_platforms.py +++ b/plugin/idg/browser/remote_platforms.py @@ -4,15 +4,14 @@ from qgis.core import QgsProject from qgis.PyQt.QtGui import QIcon -from idg.toolbelt import PlgOptionsManager, PluginGlobals +from idg.toolbelt import PlgOptionsManager +from idg.plugin_globals import PluginGlobals class RemotePlatforms: def __init__(self, read_projects=True): self.plateforms = [] - with open( - os.path.join(PluginGlobals.instance().config_dir_path, "default_idg.json") - ) as f: # Télécharger si non existant ? + with open(PluginGlobals.CONFIG_FILE_PATH) as f: self.stock_idgs = json.load(f) self.custom_idg = PlgOptionsManager().get_plg_settings().custom_idgs.split(",") self.custom_idg.remove("") @@ -37,9 +36,7 @@ def url_stock(self): class Plateform: - def __init__( - self, url, idg_id, read_project=True - ): + def __init__(self, url, idg_id, read_project=True): self.url = url self.idg_id = idg_id if read_project: @@ -49,7 +46,7 @@ def read_project(self): p = QgsProject() if ( p.read( - self.qgis_project_filepath(), + str(self.qgis_project_filepath()), QgsProject.ReadFlags() | QgsProject.FlagDontResolveLayers | QgsProject.FlagDontLoadLayouts, @@ -61,10 +58,9 @@ def read_project(self): def qgis_project_filepath(self): suffix = os.path.splitext(os.path.basename(self.url))[-1] # .qgs ou .qgz - local_file_name = os.path.join( - PluginGlobals.instance().config_dir_path, self.idg_id + suffix - ) - return local_file_name + local_file_name = self.idg_id + suffix + local_file_path = PluginGlobals.CONFIG_DIR_PATH / local_file_name + return local_file_path def is_custom(self): # Comparer avec les pf stock @@ -96,11 +92,9 @@ def icon(self): for link in self.project.metadata().links(): if link.name.lower().strip() == "icon": icon_suffix = os.path.splitext(os.path.basename(link.url))[-1] + icon_file_name = str(self.idg_id) + icon_suffix return QIcon( - os.path.join( - PluginGlobals.instance().config_dir_path, - str(self.idg_id) + icon_suffix, - ) + str(PluginGlobals.CONFIG_DIR_PATH / icon_file_name) ) return None diff --git a/plugin/idg/toolbelt/tree_node_factory.py b/plugin/idg/browser/tree_node_factory.py similarity index 51% rename from plugin/idg/toolbelt/tree_node_factory.py rename to plugin/idg/browser/tree_node_factory.py index c51970a..3562abd 100644 --- a/plugin/idg/toolbelt/tree_node_factory.py +++ b/plugin/idg/browser/tree_node_factory.py @@ -1,23 +1,19 @@ # -*- coding: utf-8 -*- -import os +from pathlib import Path from qgis.core import QgsProject from qgis.PyQt.QtCore import QThread, pyqtSignal -from idg.toolbelt import PluginGlobals -from .network_manager import NetworkRequestsManager +from idg.plugin_globals import PluginGlobals +from idg.browser.network_manager import NetworkRequestsManager class DownloadDefaultIdgListAsync(QThread): finished = pyqtSignal() - def __init__( - self, - url="https://raw.githubusercontent.com/geo2france/idg-qgis-plugin/dev/plugin/idg/config" - "/default_idg.json", - ): + def __init__(self, url: str): super(QThread, self).__init__() self.url = url @@ -25,12 +21,12 @@ def run(self): qntwk = NetworkRequestsManager() qntwk.download_file( self.url, - os.path.join(PluginGlobals.instance().config_dir_path, "default_idg.json"), + str(PluginGlobals.CONFIG_FILE_PATH), ) self.finished.emit() -class DownloadAllConfigFilesAsync(QThread): +class DownloadAllIdgFilesAsync(QThread): finished = pyqtSignal() def __init__(self, idgs): @@ -43,28 +39,25 @@ def run(self): for idg_id, url in self.idgs.items(): # continue si l'IDG est masquée idg_id = str(idg_id) - suffix = os.path.splitext(os.path.basename(url))[-1] - local_file_name = qntwk.download_file( - url, - os.path.join(PluginGlobals.instance().config_dir_path, idg_id + suffix), - ) - if local_file_name: + suffix = Path(url).suffix + local_file_name = idg_id + suffix + local_file_path = PluginGlobals.CONFIG_DIR_PATH / local_file_name + local_file = qntwk.download_file(url, str(local_file_path)) + if local_file: project = QgsProject() project.read( - local_file_name, + local_file, QgsProject.ReadFlags() | QgsProject.FlagDontResolveLayers | QgsProject.FlagDontLoadLayouts, ) for link in project.metadata().links(): if link.name.lower().strip() == "icon": - suffix = os.path.splitext(os.path.basename(link.url))[-1] - qntwk.download_file( - link.url, - os.path.join( - PluginGlobals.instance().config_dir_path, - idg_id + suffix, - ), + icon_suffix = Path(link.url).suffix + icon_file_name = idg_id + icon_suffix + icon_file_path = ( + PluginGlobals.CONFIG_DIR_PATH / icon_file_name ) + qntwk.download_file(link.url, str(icon_file_path)) break self.finished.emit() diff --git a/plugin/idg/config/default_idg.json b/plugin/idg/config/default_idg.json index 1ec7f85..905f0c0 100644 --- a/plugin/idg/config/default_idg.json +++ b/plugin/idg/config/default_idg.json @@ -1,7 +1,7 @@ { - "Géoplateforme": "https://raw.githubusercontent.com/Geoplateforme/plugin_idg_gpf/master/projet_idg_gpf.qgz", - "geo2france": "https://raw.githubusercontent.com/geo2france/idg-qgis-plugin-g2f/main/geo2france.qgz", - "OPenIG": "https://raw.githubusercontent.com/openig/Plugin-QGIS3-OPenIG/master/OPenIG_qgis_plugin.qgs", "DataGrandEst": "https://www.datagrandest.fr/tools/plugin-qgis-datagrandest/config-plugin-idg-datagrandest.qgz", - "GéoBretagne": "https://geobretagne.fr/pub/plugin-qgis/config-idg-geobretagne.qgs" -} + "Géo2France": "https://raw.githubusercontent.com/geo2france/idg-qgis-plugin-g2f/main/geo2france.qgz", + "GéoBretagne": "https://geobretagne.fr/pub/plugin-qgis/config-idg-geobretagne.qgz", + "Géoplateforme": "https://raw.githubusercontent.com/Geoplateforme/plugin_idg_gpf/master/projet_idg_gpf.qgz", + "OPenIG": "https://raw.githubusercontent.com/openig/Plugin-QGIS3-OPenIG/master/OPenIG_qgis_plugin.qgs" +} \ No newline at end of file diff --git a/plugin/idg/gui/actions.py b/plugin/idg/gui/actions.py new file mode 100644 index 0000000..aea5ade --- /dev/null +++ b/plugin/idg/gui/actions.py @@ -0,0 +1,13 @@ +""" +Plugin actions. +""" + +from qgis.PyQt.QtWidgets import QAction + + +class PluginActions: + """Container for plugin actions shared in several places of the plugin.""" + + action_show_help: QAction + action_show_settings: QAction + action_reload_idgs: QAction diff --git a/plugin/idg/gui/dlg_settings.py b/plugin/idg/gui/dlg_settings.py index e928ddd..99067b0 100644 --- a/plugin/idg/gui/dlg_settings.py +++ b/plugin/idg/gui/dlg_settings.py @@ -1,35 +1,31 @@ #! python3 # noqa: E265 """ - Plugin settings form integrated into QGIS 'Options' menu. +Plugin settings form integrated into QGIS 'Options' menu. """ # standard from functools import partial from pathlib import Path -import os.path -import json # PyQGIS -from qgis.core import QgsApplication, Qgis +from qgis.core import QgsApplication from qgis.gui import QgsOptionsPageWidget, QgsOptionsWidgetFactory -from qgis.utils import iface from qgis.PyQt import uic, QtWidgets -from qgis.PyQt.Qt import QUrl, QWidget +from qgis.PyQt.Qt import QUrl from qgis.PyQt.QtGui import QDesktopServices, QIcon # project from idg.__about__ import ( - DIR_PLUGIN_ROOT, __icon_path__, __title__, __uri_homepage__, __uri_tracker__, __version__, ) -from idg.toolbelt import PlgLogger, PlgOptionsManager, PluginGlobals, RemotePlatforms, IdgProvider +from idg.toolbelt import PlgLogger, PlgOptionsManager +from idg.browser.remote_platforms import RemotePlatforms from idg.toolbelt.preferences import PlgSettingsStructure -from idg.toolbelt.tree_node_factory import DownloadAllConfigFilesAsync # ############################################################################ # ########## Globals ############### @@ -74,7 +70,6 @@ def __init__(self, parent): super().__init__(parent) self.log = PlgLogger().log self.plg_settings = PlgOptionsManager() - settings = self.plg_settings.get_plg_settings() # load UI and set objectName self.setupUi(self) @@ -84,52 +79,80 @@ def __init__(self, parent): self.lbl_title.setText(f"{__title__} - Version {__version__}") # customization - self.btn_help.setIcon(QIcon(QgsApplication.iconPath("mActionHelpContents.svg"))) + self.btn_help.setIcon(QgsApplication.getThemeIcon("mActionHelpContents.svg")) self.btn_help.pressed.connect( partial(QDesktopServices.openUrl, QUrl(__uri_homepage__)) ) self.btn_report.setIcon( - QIcon(QgsApplication.iconPath("console/iconSyntaxErrorConsole.svg")) + QIcon(":images/themes/default/console/iconSyntaxErrorConsole.svg") ) self.btn_report.pressed.connect( partial(QDesktopServices.openUrl, QUrl(f"{__uri_tracker__}")) ) - self.btn_reset.setIcon(QIcon(QgsApplication.iconPath("mActionUndo.svg"))) + self.btn_reset.setIcon(QgsApplication.getThemeIcon("mActionUndo.svg")) self.btn_reset.pressed.connect(self.reset_settings) - # table widget - self.idgs_list.horizontalHeader().setSectionResizeMode( + # Hide non operational widgets + # todo: make them work + self.lbl_custom_platforms.hide() + self.tbl_platforms_list.hide() + self.btn_add_platform.hide() + + # Custom IDGs list + self.tbl_platforms_list.horizontalHeader().setSectionResizeMode( QtWidgets.QHeaderView.Stretch - ) # Etirer la colonne à 100% du tableau - self.btn_addrow.setIcon(QIcon(":images/themes/default/symbologyAdd.svg")) - self.btn_addrow.clicked.connect( - lambda: self.idgs_list.setRowCount(self.idgs_list.rowCount() + 1) + ) # Stretch the column in order to use the full width of the table + + # Button to add a custom IDG + self.btn_add_platform.setIcon(QgsApplication.getThemeIcon("symbologyAdd.svg")) + self.btn_add_platform.clicked.connect( + lambda: self.tbl_platforms_list.setRowCount( + self.tbl_platforms_list.rowCount() + 1 + ) ) - # Lire la config pour voir quels sont les PF masquées self.vbox = QtWidgets.QVBoxLayout() self.checkboxes = [] self.groupBox_stock.setLayout(self.vbox) - for k in RemotePlatforms(read_projects=False).stock_idgs.keys(): - cb = QtWidgets.QCheckBox(k) - self.vbox.addWidget(cb) - self.checkboxes.append(cb) # load previously saved settings self.load_settings() + def _update_default_idgs_list(self): + self.checkboxes = [] + + # Clear content of layout + for i in reversed(range(self.vbox.count())): + self.vbox.itemAt(i).widget().setParent(None) + + # Create checkboxes + for pf_name in RemotePlatforms(read_projects=False).stock_idgs.keys(): + cb = QtWidgets.QCheckBox(pf_name) + self.vbox.addWidget(cb) + self.checkboxes.append(cb) + + # Set values to checkboxes + settings = self.plg_settings.get_plg_settings() + hidden_idg = settings.hidden_idgs.split(",") + for cb in self.checkboxes: + if cb.text() in hidden_idg: + cb.setChecked(False) + else: + cb.setChecked(True) + def apply(self): """Called to permanently apply the settings shown in the options page (e.g. \ save them to QgsSettings objects). This is usually called when the options \ dialog is accepted.""" settings = self.plg_settings.get_plg_settings() - # misc + # Misc settings.version = __version__ - settings.custom_idgs = ",".join(tablewidgetToList(self.idgs_list, 0)) + settings.custom_idgs = ",".join(tablewidgetToList(self.tbl_platforms_list, 0)) + # Default IDG list hidden__idgs_arr = [] for cb in self.checkboxes: print(cb.text(), cb.checkState()) @@ -138,54 +161,58 @@ def apply(self): # Add to hidden PF settings.hidden_idgs = ",".join(hidden__idgs_arr) - # dump new settings into QgsSettings - self.plg_settings.save_from_object( - settings - ) # Les variables globales ne sont peut être pas MAJ ici - - items = {c.idg_id : c.url for c in RemotePlatforms(read_projects=False).plateforms if not c.is_hidden()} - registry = QgsApplication.instance().dataItemProviderRegistry() - provider = registry.provider(IdgProvider().name()) - self.task = DownloadAllConfigFilesAsync(items) # Download non-hidden idg - self.task.finished.connect(provider.root.refresh) - self.task.start() + # Dump new settings into QgsSettings + self.plg_settings.save_from_object(settings) + if __debug__: self.log( message="DEBUG - Settings successfully saved.", log_level=4, ) + # Send signal to plugin + self.settings_updated() + + def plugin_config_file_reloaded(self): + self._update_default_idgs_list() + def load_settings(self): """Load options from QgsSettings into UI form.""" settings = self.plg_settings.get_plg_settings() - hidden_idg = settings.hidden_idgs.split(",") - for c in self.checkboxes: - if c.text() in hidden_idg: - c.setChecked(False) - else: - c.setChecked(True) - self.idgs_list.setRowCount(len(settings.custom_idgs.split(",")) + 1) + + # Default IDG list + self._update_default_idgs_list() + + # Custom IDG list + self.tbl_platforms_list.setRowCount(len(settings.custom_idgs.split(",")) + 1) listToTablewidget( - settings.custom_idgs.split(","), self.idgs_list, column_index=0 + settings.custom_idgs.split(","), self.tbl_platforms_list, column_index=0 ) + # Version of the plugin used to save the settings + self.lbl_version_saved_value.setText(settings.version) + def reset_settings(self): """Reset settings to default values (set in preferences.py module).""" default_settings = PlgSettingsStructure() - # dump default settings into QgsSettings + # Dump default settings into QgsSettings self.plg_settings.save_from_object(default_settings) self.load_settings() - provider = QgsApplication.instance().dataItemProviderRegistry().provider("IDG Provider") - provider.root.refresh() + + # Call download function + self.download_tree_config_file(end_slot=self.plugin_config_file_reloaded) + class PlgOptionsFactory(QgsOptionsWidgetFactory): """Factory for options widget.""" - def __init__(self): + def __init__(self, settings_updated=None, download_tree_config_file=None): """Constructor.""" super().__init__() + self.settings_updated = settings_updated + self.download_tree_config_file = download_tree_config_file def icon(self) -> QIcon: """Returns plugin icon, used to as tab icon in QGIS options tab widget. @@ -204,7 +231,15 @@ def createWidget(self, parent) -> ConfigOptionsPage: :return: options page for tab widget :rtype: ConfigOptionsPage """ - return ConfigOptionsPage(parent) + options_page = ConfigOptionsPage(parent) + + # Plugin functions to be called on dlg_settings events + if self.settings_updated: + options_page.settings_updated = self.settings_updated + if self.download_tree_config_file: + options_page.download_tree_config_file = self.download_tree_config_file + + return options_page def title(self) -> str: """Returns plugin title, used to name the tab in QGIS options tab widget. diff --git a/plugin/idg/gui/dlg_settings.ui b/plugin/idg/gui/dlg_settings.ui index d50b1b7..c000913 100644 --- a/plugin/idg/gui/dlg_settings.ui +++ b/plugin/idg/gui/dlg_settings.ui @@ -7,7 +7,7 @@ 0 0 715 - 561 + 560 @@ -60,22 +60,72 @@ + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + Version used to save settings: + + + + + + + version x.y.z + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + Default plateforms + + false + - + Custom plateforms - + false @@ -107,106 +157,93 @@ - + - ... + - - + + + Qt::Vertical + + - 0 - 100 + 20 + 40 - - - - - Miscellaneous + + + + + + Qt::Horizontal - - false + + + + + + + + true + + + + + + Help… + + + + + + + true + + + + + + Report an issue… + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + true + + + true + + + Reset setttings to factory defaults + + + + + + + + + Qt::Horizontal - - - - - true - - - - 200 - 25 - - - - - 500 - 30 - - - - - - - Help - - - - - - - true - - - - 200 - 25 - - - - - 16777215 - 30 - - - - true - - - Reset setttings to factory defaults - - - - - - - true - - - - 200 - 25 - - - - - 500 - 30 - - - - - - - Report an issue - - - - diff --git a/plugin/idg/metadata.txt b/plugin/idg/metadata.txt index f195827..3014142 100644 --- a/plugin/idg/metadata.txt +++ b/plugin/idg/metadata.txt @@ -24,5 +24,12 @@ qgisMinimumVersion=3.00 qgisMaximumVersion=3.99 # versioning -version=0.2.4 -changelog=Bugfix, Amélioration performances +version=0.2.5 +changelog= - Ajout d'un paramètre pour activer/désactiver le téléchargement du fichier de configuration au lancement (par défaut True) (config_files_download_at_startup) + - Ajout d'un bouton pour forcer le rechargement des fichiers distants + - Retrait des éléments non fonctionnels du panneau de configuration (plateformes custom) + - Suppression des paramètres obsolètes du plugin (qui étaient accessibles via la fenêtre des paramètres de QGIS) + - Réorganisation du code en vue d'une maintenance plus facile + - Amélioration de l'organisation des éléments dans la fenêtre de paramétrage du plugin + - Affichage de la liste des IDG dans l'ordre alphabétique (dans la fenêtre de paramétrage) + - Utilisation de la bonne orthographe pour "Géo2France" diff --git a/plugin/idg/plugin_globals.py b/plugin/idg/plugin_globals.py new file mode 100644 index 0000000..1ac70ce --- /dev/null +++ b/plugin/idg/plugin_globals.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- + +from idg.__about__ import DIR_PLUGIN_ROOT +from pathlib import Path + + +class PluginGlobals: + """ """ + + PLUGIN_TAG: str = "IDG" + CONFIG_DIR_NAME: str = "config" + DEFAULT_CONFIG_FILE_NAME: str = "default_idg.json" + BROWSER_PROVIDER_NAME: str = "IDG Provider" + CONFIG_DIR_PATH: Path + CONFIG_FILE_PATH: Path + PLUGIN_PATH: Path + + def __init__(self): + self.init_constants() + + @classmethod + def init_constants(cls): + """ + Init calculated class variables + """ + + PluginGlobals.PLUGIN_PATH = DIR_PLUGIN_ROOT.resolve() + + PluginGlobals.CONFIG_DIR_PATH = ( + PluginGlobals.PLUGIN_PATH / PluginGlobals.CONFIG_DIR_NAME + ).resolve() + PluginGlobals.CONFIG_FILE_PATH = ( + PluginGlobals.CONFIG_DIR_PATH / PluginGlobals.DEFAULT_CONFIG_FILE_NAME + ).resolve() diff --git a/plugin/idg/plugin_main.py b/plugin/idg/plugin_main.py index e0fa8c4..3031580 100644 --- a/plugin/idg/plugin_main.py +++ b/plugin/idg/plugin_main.py @@ -1,7 +1,7 @@ #! python3 # noqa: E265 """ - Main plugin module. +Main plugin module. """ # PyQGIS @@ -17,17 +17,16 @@ from idg.gui.dlg_settings import PlgOptionsFactory from idg.toolbelt import PlgLogger, PlgTranslator - - -from idg.toolbelt import PluginGlobals, IdgProvider -from idg.toolbelt.remote_platforms import RemotePlatforms -from idg.toolbelt.tree_node_factory import ( - DownloadAllConfigFilesAsync, +from idg.toolbelt import PlgOptionsManager +from idg.plugin_globals import PluginGlobals +from idg.gui.actions import PluginActions +from idg.browser.remote_platforms import RemotePlatforms +from idg.browser.browser import IdgProvider +from idg.browser.tree_node_factory import ( + DownloadAllIdgFilesAsync, DownloadDefaultIdgListAsync, ) -import os -import json # ############################################################################ # ########## Classes ############### @@ -51,14 +50,7 @@ def __init__(self, iface: QgisInterface): QCoreApplication.installTranslator(translator) self.tr = plg_translation_mngr.tr - PluginGlobals.instance().set_plugin_path( - os.path.dirname(os.path.abspath(__file__)) - ) - # PluginGlobals.instance().set_plugin_iface(self.iface) - PluginGlobals.instance().reload_globals_from_qgis_settings() - - config_struct = None - config_string = "" + PluginGlobals.init_constants() self.registry = QgsApplication.instance().dataItemProviderRegistry() self.provider = IdgProvider(self.iface) @@ -68,55 +60,43 @@ def __init__(self, iface: QgisInterface): def post_ui_init(self): """Run after plugin's UI has been initialized.""" - items ={c.idg_id : c.url for c in RemotePlatforms(read_projects=False).plateforms if not c.is_hidden()} - self.task1 = DownloadDefaultIdgListAsync() - self.task2 = DownloadAllConfigFilesAsync( - items - ) - self.task1.finished.connect(self.task2.start) - self.task2.finished.connect(lambda : self.registry.addProvider(self.provider)) - - self.task1.start() + self.registry.addProvider(self.provider) - def need_download_tree_config_file(self): - """ - Do we need to download a new version of the resources tree file? - 2 possible reasons: - - the user wants it to be downloading at plugin start up - - the file is currently missing - """ - - return ( - PluginGlobals.instance().CONFIG_FILES_DOWNLOAD_AT_STARTUP > 0 - or not os.path.isfile(PluginGlobals.instance().config_file_path) - ) + self.download_all_config_files() def initGui(self): """Set up plugin UI elements.""" # settings page within the QGIS preferences menu - self.options_factory = PlgOptionsFactory() + self.options_factory = PlgOptionsFactory( + self.settings_updated_slot, self.download_tree_config_file_slot + ) self.iface.registerOptionsWidgetFactory(self.options_factory) # -- Actions - self.action_help = QAction( - QIcon(":/images/themes/default/mActionHelpContents.svg"), - self.tr("Help", context="IdgPlugin"), + PluginActions.action_show_help = QAction( + QgsApplication.getThemeIcon("mActionHelpContents.svg"), + self.tr("Help…", context="IdgPlugin"), self.iface.mainWindow(), ) - self.action_help.triggered.connect( + PluginActions.action_show_help.triggered.connect( lambda: showPluginHelp(filename="resources/help/index") ) - self.action_settings = QAction( + PluginActions.action_show_settings = QAction( QgsApplication.getThemeIcon("console/iconSettingsConsole.svg"), - self.tr("Settings"), + self.tr("Settings…"), self.iface.mainWindow(), ) - self.action_settings.triggered.connect( - lambda: self.iface.showOptionsDialog( - currentPage="mOptionsPage{}".format(__title__) - ) + PluginActions.action_show_settings.triggered.connect(self.show_settings_dialog) + + PluginActions.action_reload_idgs = QAction( + QgsApplication.getThemeIcon("mActionRefresh.svg"), + self.tr("Reload files"), + self.iface.mainWindow(), + ) + PluginActions.action_reload_idgs.triggered.connect( + self.download_all_config_files ) # -- Menu @@ -124,19 +104,22 @@ def initGui(self): # Create a menu self.createPluginMenu() + def show_settings_dialog(self): + self.iface.showOptionsDialog(currentPage="mOptionsPage{}".format(__title__)) def unload(self): """Cleans up when plugin is disabled/uninstalled.""" # -- Clean up menu - self.iface.removePluginMenu(__title__, self.action_help) - self.iface.removePluginMenu(__title__, self.action_settings) + self.iface.removePluginMenu(__title__, PluginActions.action_show_help) + self.iface.removePluginMenu(__title__, PluginActions.action_show_settings) # -- Clean up preferences panel in QGIS settings self.iface.unregisterOptionsWidgetFactory(self.options_factory) # remove actions - del self.action_settings - del self.action_help + del PluginActions.action_show_settings + del PluginActions.action_reload_idgs + del PluginActions.action_show_help """ Removes the plugin menu """ @@ -153,30 +136,88 @@ def createPluginMenu(self): self.plugin_menu = QMenu(__title__, plugin_menu) plugin_menu.addMenu(self.plugin_menu) - self.plugin_menu.addAction(self.action_settings) - self.plugin_menu.addAction(self.action_help) + self.plugin_menu.addAction(PluginActions.action_show_settings) + self.plugin_menu.addAction(PluginActions.action_reload_idgs) + self.plugin_menu.addAction(PluginActions.action_show_help) - def run(self): - """Main process. + def settings_updated_slot(self): + """Function called when the settings are updated""" - :raises Exception: if there is no item in the feed + self.download_all_config_files() + + def _get_active_remote_plateforms(self): + """Get the list of the active platforms (non-hidden ones).""" + active_platforms = { + pf.idg_id: pf.url + for pf in RemotePlatforms(read_projects=False).plateforms + if not pf.is_hidden() + } + + return active_platforms + + def _need_download_tree_config_file(self): """ - # Jamais utilisé ? - try: - self.log( - message=self.tr( - text="Everything ran OK.", - context="IdgPlugin", - ), - log_level=3, - push=False, - ) - except Exception as err: + Do we need to download a new version of the resources tree file? + 2 possible reasons: + - the user wants it to be downloading at plugin start up + - the file is currently missing + """ + config_file_exists = PluginGlobals.CONFIG_FILE_PATH.is_file() + + settings = PlgOptionsManager().get_plg_settings() + + return settings.download_files_at_startup > 0 or not config_file_exists + + def download_all_config_files(self, end_slot=None): + """Download the plugin config file and all the files of the active platforms. + Hidden platform files are not downloaded.""" + + self.log( + message="DEBUG - prepare threads for downloading files...", + log_level=4, + ) + + active_platforms = self._get_active_remote_plateforms() + + settings = PlgOptionsManager().get_plg_settings() + config_file_url = settings.config_file_url + + if not end_slot: + end_slot = self.refresh_data_provider + + self.task1 = DownloadDefaultIdgListAsync(url=config_file_url) + self.task2 = DownloadAllIdgFilesAsync(active_platforms) + self.task1.finished.connect(self.task2.start) + self.task2.finished.connect(end_slot) + + self.task1.start() + + def download_tree_config_file_slot(self, file_url=None, end_slot=None): + """Download the plugin config file. + Platform files are not downloaded.""" + + self.log( + message="DEBUG - prepare thread for downloading plugin config file...", + log_level=4, + ) + + settings = PlgOptionsManager().get_plg_settings() + config_file_url = file_url or settings.config_file_url + + if not end_slot: + end_slot = self.refresh_data_provider + + self.task1 = DownloadDefaultIdgListAsync(url=config_file_url) + self.task1.finished.connect(end_slot) + + self.task1.start() + + def refresh_data_provider(self): + if __debug__: self.log( - message=self.tr( - text="Houston, we've got a problem: {}".format(err), - context="IdgPlugin", - ), - log_level=2, - push=True, + message="DEBUG - refresh_data_provider.", + log_level=4, ) + + if self.provider and self.provider.root: + self.provider.root.refresh() diff --git a/plugin/idg/resources/help/index.html b/plugin/idg/resources/help/index.html index 89434c4..25f61bf 100644 --- a/plugin/idg/resources/help/index.html +++ b/plugin/idg/resources/help/index.html @@ -3,13 +3,13 @@ - Redirecting... + Redirecting… -

Redirection to the online documentation...

+

Redirection to the online documentation…

diff --git a/plugin/idg/resources/i18n/idg_fr.qm b/plugin/idg/resources/i18n/idg_fr.qm index 05bf2c5..63510b7 100644 Binary files a/plugin/idg/resources/i18n/idg_fr.qm and b/plugin/idg/resources/i18n/idg_fr.qm differ diff --git a/plugin/idg/resources/i18n/idg_fr.ts b/plugin/idg/resources/i18n/idg_fr.ts index bf59f2e..e9a83a4 100644 --- a/plugin/idg/resources/i18n/idg_fr.ts +++ b/plugin/idg/resources/i18n/idg_fr.ts @@ -4,9 +4,9 @@ @default - - Settings... - Paramètres... + + Settings… + Paramètres… @@ -17,71 +17,76 @@ Formulaire - - Miscellaneous - Divers + + Report an issue… + Signaler un bug… - - Report an issue - Signaler un bug - - - + Version used to save settings: - Version utilisées pour sauvegarder la configuration: + Version utilisée pour sauvegarder la configuration : - - Help - Aide + + Help… + Aide… - + Reset setttings to factory defaults Réinitialiser les paramètres par défaut - + Default plateforms - Plateformes IDG + Plateformes par défaut - + Custom plateforms Plateformes supplémentaires + + + version x.y.z + + IdgPlugin - - Help - Aide + + Help… + Aide… + + + + Settings… + Paramètres… - - Settings - Paramètres + + Reload files + Recharger les fichiers LayerItem - - Show metadata - Voir les métadonnées + + Show metadata… + Afficher les métadonnées… - - Display layer - Afficher la couche + + Add layer to map + Ajouter la couche à la carte PlatformCollection - + Hide Masquer @@ -89,19 +94,19 @@ RootCollection - - Settings... - Paramètres... + + Settings… + Paramètres… - + Plateforms Plateformes - - Add URL - Ajouter une URL + + Add URL… + Ajouter une URL… diff --git a/plugin/idg/resources/i18n/plugin_translation.pro b/plugin/idg/resources/i18n/plugin_translation.pro index 9d1496e..4ff4a24 100644 --- a/plugin/idg/resources/i18n/plugin_translation.pro +++ b/plugin/idg/resources/i18n/plugin_translation.pro @@ -1,5 +1,6 @@ FORMS = ../../gui/dlg_settings.ui -SOURCES= ../../plugin_main.py ../../toolbelt/browser.py +SOURCES= ../../plugin_main.py \ + ../../browser/browser.py TRANSLATIONS = idg_fr.ts diff --git a/plugin/idg/toolbelt/__init__.py b/plugin/idg/toolbelt/__init__.py index 841860d..571a794 100644 --- a/plugin/idg/toolbelt/__init__.py +++ b/plugin/idg/toolbelt/__init__.py @@ -2,8 +2,3 @@ from .log_handler import PlgLogger # noqa: F401 from .preferences import PlgOptionsManager # noqa: F401 from .translator import PlgTranslator # noqa: F401 -from .plugin_globals import PluginGlobals -from .singleton import Singleton -from .browser import IdgProvider -from .remote_platforms import RemotePlatforms -from .network_manager import NetworkRequestsManager # noqa: F401 diff --git a/plugin/idg/toolbelt/log_handler.py b/plugin/idg/toolbelt/log_handler.py index e41f7ce..789c8dd 100644 --- a/plugin/idg/toolbelt/log_handler.py +++ b/plugin/idg/toolbelt/log_handler.py @@ -129,7 +129,7 @@ def log( notification = iface.messageBar().createMessage( title=application, text=message ) - widget_button = QPushButton(button_text or "More...") + widget_button = QPushButton(button_text or "More…") if button_connect: widget_button.clicked.connect(button_connect) else: diff --git a/plugin/idg/toolbelt/plugin_globals.py b/plugin/idg/toolbelt/plugin_globals.py deleted file mode 100644 index 472373a..0000000 --- a/plugin/idg/toolbelt/plugin_globals.py +++ /dev/null @@ -1,62 +0,0 @@ -# -*- coding: utf-8 -*- - -import sys -import os -import json -from .singleton import Singleton -from .preferences import PlgOptionsManager -from qgis.PyQt.QtCore import QSettings - - -@Singleton -class PluginGlobals: - """ """ - - iface = None - plugin_path = None - PLUGIN_TAG = "IDG" - CONFIG_DIR_NAME = "config" - CONFIG_FILE_NAMES = ["projet_idg.qgs"] - CONFIG_FILES_DOWNLOAD_AT_STARTUP = PlgOptionsManager().get_value_from_key( - "config_files_download_at_startup" - ) - - def __init__(self): - self.default_qsettings = {"CONFIG_FILE_NAMES": self.CONFIG_FILE_NAMES} - self.config_dir_path = None - self.config_file_path = None - self.images_dir_path = None - self.logo_file_path = None - - def set_plugin_path(self, plugin_path): - self.plugin_path = plugin_path - - def reload_globals_from_qgis_settings(self): - """ - Reloads the global variables of the plugin - """ - - # Read the qgis plugin settings - s = QSettings() - self.CONFIG_FILES_DOWNLOAD_AT_STARTUP = ( - True - if s.value( - "{0}/config_files_download_at_startup".format(self.PLUGIN_TAG), - self.CONFIG_FILES_DOWNLOAD_AT_STARTUP, - ) - == "1" - else False - ) - - self.CONFIG_DIR_NAME = s.value( - "{0}/config_dir_name".format(self.PLUGIN_TAG), self.CONFIG_DIR_NAME - ) - - self.CONFIG_FILE_NAMES = s.value( - "{0}/config_file_names".format(self.PLUGIN_TAG), self.CONFIG_FILE_NAMES - ) - - self.config_dir_path = os.path.join(self.plugin_path, self.CONFIG_DIR_NAME) - self.config_file_path = os.path.join( - self.config_dir_path, self.CONFIG_FILE_NAMES[0] - ) diff --git a/plugin/idg/toolbelt/preferences.py b/plugin/idg/toolbelt/preferences.py index 34e8ae1..e48421a 100644 --- a/plugin/idg/toolbelt/preferences.py +++ b/plugin/idg/toolbelt/preferences.py @@ -1,20 +1,20 @@ #! python3 # noqa: E265 """ - Plugin settings. +Plugin settings. """ # standard from dataclasses import asdict, dataclass, fields -import json # PyQGIS from qgis.core import QgsSettings from qgis.PyQt.QtCore import QVariant # package -import idg.toolbelt.log_handler as log_hdlr +import idg.toolbelt from idg.__about__ import __title__, __version__ +from idg.plugin_globals import PluginGlobals # ############################################################################ # ########## Classes ############### @@ -29,11 +29,13 @@ class PlgSettingsStructure: debug_mode: bool = False version: str = __version__ configs_folder: str = "" - config_files_download_at_startup: bool = False - hide_empty_groups: bool = True - hide_resources_with_warn_status: bool = True + download_files_at_startup: bool = True custom_idgs: str = "" hidden_idgs: str = "" + config_file_url: str = ( + "https://raw.githubusercontent.com/geo2france/idg-qgis-plugin/" + "dev/plugin/idg/config/default_idg.json" + ) class PlgOptionsManager: @@ -76,7 +78,7 @@ def get_value_from_key( :return: plugin settings value matching key """ if not hasattr(PlgSettingsStructure, key): - log_hdlr.PlgLogger.log( + idg.toolbelt.log_handler.PlgLogger.log( message="Bad settings key. Must be one of: {}".format( ",".join(PlgSettingsStructure._fields) # A fixer ), @@ -90,7 +92,7 @@ def get_value_from_key( try: out_value = settings.value(key=key, defaultValue=default, type=exp_type) except Exception as err: - log_hdlr.PlgLogger.log( + idg.toolbelt.log_handler.PlgLogger.log( message="Error occurred trying to get settings: {}.Trace: {}".format( key, err ) @@ -113,7 +115,7 @@ def set_value_from_key(cls, key: str, value) -> bool: :rtype: bool """ if not hasattr(PlgSettingsStructure, key): - log_hdlr.PlgLogger.log( + idg.toolbelt.log_handler.PlgLogger.log( message="Bad settings key. Must be one of: {}".format( ",".join(PlgSettingsStructure._fields) ), @@ -128,7 +130,7 @@ def set_value_from_key(cls, key: str, value) -> bool: settings.setValue(key, value) out_value = True except Exception as err: - log_hdlr.PlgLogger.log( + idg.toolbelt.log_handler.PlgLogger.log( message="Error occurred trying to set settings: {}.Trace: {}".format( key, err ) diff --git a/plugin/idg/toolbelt/singleton.py b/plugin/idg/toolbelt/singleton.py deleted file mode 100644 index b286056..0000000 --- a/plugin/idg/toolbelt/singleton.py +++ /dev/null @@ -1,45 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -From http://stackoverflow.com/a/7346105 -""" - - -class Singleton: - """ - A non-thread-safe helper class to ease implementing singletons. - This should be used as a decorator -- not a metaclass -- to the - class that should be a singleton. - - The decorated class can define one `__init__` function that - takes only the `self` argument. Other than that, there are - no restrictions that apply to the decorated class. - - To get the singleton instance, use the `instance` method. Trying - to use `__call__` will result in a `TypeError` being raised. - - Limitations: The decorated class cannot be inherited from. - - """ - - def __init__(self, decorated): - self._decorated = decorated - - def instance(self): - """ - Returns the singleton instance. Upon its first call, it creates a - new instance of the decorated class and calls its `__init__` method. - On all subsequent calls, the already created instance is returned. - - """ - try: - return self._instance - except AttributeError: - self._instance = self._decorated() - return self._instance - - def __call__(self): - raise TypeError("Singletons must be accessed through `instance()`.") - - def __instancecheck__(self, inst): - return isinstance(inst, self._decorated)