From 99c3c84e1170f997421dc5bb5450c8e1c8fcd641 Mon Sep 17 00:00:00 2001 From: Niko Aarnio Date: Mon, 3 Feb 2025 11:50:27 +0200 Subject: [PATCH 1/5] import iface always from utils to avoid need to cast type multiple times --- arho_feature_template/gui/docks/validation_dock.py | 3 +-- arho_feature_template/plugin.py | 9 ++------- arho_feature_template/project/layers/plan_layers.py | 3 +-- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/arho_feature_template/gui/docks/validation_dock.py b/arho_feature_template/gui/docks/validation_dock.py index 222c64b..9658d1c 100644 --- a/arho_feature_template/gui/docks/validation_dock.py +++ b/arho_feature_template/gui/docks/validation_dock.py @@ -5,10 +5,9 @@ from qgis.gui import QgsDockWidget from qgis.PyQt import uic -from qgis.utils import iface from arho_feature_template.core.lambda_service import LambdaService -from arho_feature_template.utils.misc_utils import get_active_plan_id +from arho_feature_template.utils.misc_utils import get_active_plan_id, iface if TYPE_CHECKING: from qgis.PyQt.QtWidgets import QProgressBar, QPushButton diff --git a/arho_feature_template/plugin.py b/arho_feature_template/plugin.py index a10e830..ab991cd 100644 --- a/arho_feature_template/plugin.py +++ b/arho_feature_template/plugin.py @@ -1,12 +1,11 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Callable, cast +from typing import Callable from qgis.core import QgsApplication from qgis.PyQt.QtCore import QCoreApplication, Qt, QTranslator from qgis.PyQt.QtGui import QIcon from qgis.PyQt.QtWidgets import QAction, QWidget -from qgis.utils import iface from arho_feature_template.core.plan_manager import PlanManager from arho_feature_template.gui.dialogs.plugin_settings import PluginSettings @@ -14,11 +13,7 @@ from arho_feature_template.qgis_plugin_tools.tools.custom_logging import setup_logger, teardown_logger from arho_feature_template.qgis_plugin_tools.tools.i18n import setup_translation from arho_feature_template.qgis_plugin_tools.tools.resources import plugin_name, resources_path - -if TYPE_CHECKING: - from qgis.gui import QgisInterface - - iface: QgisInterface = cast("QgisInterface", iface) # type: ignore[no-redef] +from arho_feature_template.utils.misc_utils import iface class Plugin: diff --git a/arho_feature_template/project/layers/plan_layers.py b/arho_feature_template/project/layers/plan_layers.py index 67b2345..9f090da 100644 --- a/arho_feature_template/project/layers/plan_layers.py +++ b/arho_feature_template/project/layers/plan_layers.py @@ -7,7 +7,6 @@ from typing import Any, ClassVar, Generator from qgis.core import QgsExpressionContextUtils, QgsFeature, QgsProject, QgsVectorLayerUtils -from qgis.utils import iface from arho_feature_template.core.models import ( AdditionalInformation, @@ -24,7 +23,7 @@ from arho_feature_template.exceptions import FeatureNotFoundError, LayerEditableError, LayerNotFoundError from arho_feature_template.project.layers import AbstractLayer from arho_feature_template.project.layers.code_layers import PlanRegulationTypeLayer -from arho_feature_template.utils.misc_utils import LANGUAGE, get_active_plan_id +from arho_feature_template.utils.misc_utils import LANGUAGE, get_active_plan_id, iface logger = logging.getLogger(__name__) From 800b83054bba94e90af45796577c27c3343426c0 Mon Sep 17 00:00:00 2001 From: Niko Aarnio Date: Mon, 3 Feb 2025 12:17:47 +0200 Subject: [PATCH 2/5] avoid unnecessary type casting --- arho_feature_template/core/plan_manager.py | 18 +++++++-------- .../plan_regulation_group_widget.py | 22 ++++++++++++++----- .../gui/components/value_input_widgets.py | 2 +- .../gui/dialogs/plan_regulation_group_form.py | 6 +---- .../project/layers/plan_layers.py | 16 +++++--------- 5 files changed, 32 insertions(+), 32 deletions(-) diff --git a/arho_feature_template/core/plan_manager.py b/arho_feature_template/core/plan_manager.py index 3517800..9cb0db6 100644 --- a/arho_feature_template/core/plan_manager.py +++ b/arho_feature_template/core/plan_manager.py @@ -494,19 +494,18 @@ def save_plan(plan: Plan) -> QgsFeature: feature = PlanLayer.feature_from_model(plan) layer = PlanLayer.get_from_project() - editing = plan.id_ is not None _save_feature( feature=feature, layer=layer, id_=plan.id_, - edit_text="Kaavan muokkaus" if editing else "Kaavan luominen", + edit_text="Kaavan muokkaus" if plan.id_ is not None else "Kaavan luominen", ) plan_id = feature["id"] - if editing: + if plan.id_ is not None: # Check for deleted general regulations for association in RegulationGroupAssociationLayer.get_dangling_associations( - plan.general_regulations, plan_id, PlanLayer.name + plan.general_regulations, plan.id_, PlanLayer.name ): _delete_feature( association, @@ -516,7 +515,7 @@ def save_plan(plan: Plan) -> QgsFeature: # Check for documents to be deleted doc_layer = DocumentLayer.get_from_project() - for doc_feature in DocumentLayer.get_documents_to_delete(plan.documents, plan): + for doc_feature in DocumentLayer.get_documents_to_delete(plan.documents, plan.id_): _delete_feature(doc_feature, doc_layer, "Asiakirjan poisto") # Save general regulations @@ -577,26 +576,25 @@ def save_regulation_group(regulation_group: RegulationGroup, plan_id: str | None feature = RegulationGroupLayer.feature_from_model(regulation_group, plan_id) layer = RegulationGroupLayer.get_from_project() - editing = regulation_group.id_ is not None _save_feature( feature=feature, layer=layer, id_=regulation_group.id_, - edit_text="Kaavamääräysryhmän muokkaus" if editing else "Kaavamääräysryhmän lisäys", + edit_text="Kaavamääräysryhmän muokkaus" if regulation_group.id_ is not None else "Kaavamääräysryhmän lisäys", ) - if editing: + if regulation_group.id_ is not None: # Check for regulations to be deleted regulation_layer = PlanRegulationLayer.get_from_project() for reg_feature in PlanRegulationLayer.get_regulations_to_delete( - regulation_group.regulations, regulation_group + regulation_group.regulations, regulation_group.id_ ): _delete_feature(reg_feature, regulation_layer, "Kaavamääräyksen poisto") # Check for propositions to be deleted proposition_layer = PlanPropositionLayer.get_from_project() for prop_feature in PlanPropositionLayer.get_propositions_to_delete( - regulation_group.propositions, regulation_group + regulation_group.propositions, regulation_group.id_ ): _delete_feature(prop_feature, proposition_layer, "Kaavasuosituksen poisto") diff --git a/arho_feature_template/gui/components/plan_regulation_group_widget.py b/arho_feature_template/gui/components/plan_regulation_group_widget.py index cf4883c..d70f4da 100644 --- a/arho_feature_template/gui/components/plan_regulation_group_widget.py +++ b/arho_feature_template/gui/components/plan_regulation_group_widget.py @@ -1,7 +1,7 @@ from __future__ import annotations from importlib import resources -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING from qgis.core import QgsApplication from qgis.PyQt import uic @@ -10,6 +10,7 @@ from qgis.PyQt.QtWidgets import QHBoxLayout, QLabel, QSizePolicy, QWidget from arho_feature_template.core.models import PlanFeature, Proposition, Regulation, RegulationGroup +from arho_feature_template.exceptions import LayerNameNotFoundError from arho_feature_template.gui.components.plan_proposition_widget import PropositionWidget from arho_feature_template.gui.components.plan_regulation_widget import RegulationWidget from arho_feature_template.project.layers.code_layers import PlanRegulationGroupTypeLayer @@ -50,7 +51,11 @@ def __init__(self, regulation_group: RegulationGroup, plan_feature: PlanFeature) self.link_label_text: QLabel | None = None self.plan_feature = plan_feature - self.layer_name = cast(str, plan_feature.layer_name) + if not plan_feature.layer_name: + msg = "Layer name not found when creating regulation group widget!" + raise LayerNameNotFoundError(msg) + self.layer_name = plan_feature.layer_name + self.from_model(regulation_group) self.regulation_group.type_code_id = PlanRegulationGroupTypeLayer.get_id_by_feature_layer_name(self.layer_name) @@ -79,11 +84,16 @@ def from_model(self, regulation_group: RegulationGroup): self.unset_existing_regulation_group_style() if regulation_group.id_: - other_linked_features_count = len( - RegulationGroupAssociationLayer.get_associations_for_regulation_group_exclude_feature( - cast(str, regulation_group.id_), cast(str, self.plan_feature.id_), self.layer_name + if self.plan_feature.id_ is None: + other_linked_features_count = len( + list(RegulationGroupAssociationLayer.get_associations_for_regulation_group(regulation_group.id_)) + ) + else: + other_linked_features_count = len( + RegulationGroupAssociationLayer.get_associations_for_regulation_group_exclude_feature( + regulation_group.id_, self.plan_feature.id_, self.layer_name + ) ) - ) if other_linked_features_count > 0: # Set indicators that regulation group exists in the plan already and is assigned for other features self.set_existing_regulation_group_style(other_linked_features_count) diff --git a/arho_feature_template/gui/components/value_input_widgets.py b/arho_feature_template/gui/components/value_input_widgets.py index c7a37fb..0c9e086 100644 --- a/arho_feature_template/gui/components/value_input_widgets.py +++ b/arho_feature_template/gui/components/value_input_widgets.py @@ -38,7 +38,7 @@ def initialize_text_input_widget( editable: bool, # noqa: FBT001 ): if default_value: - widget.setText(str(default_value)) + widget.setText(default_value) if not editable: widget.setReadOnly(True) diff --git a/arho_feature_template/gui/dialogs/plan_regulation_group_form.py b/arho_feature_template/gui/dialogs/plan_regulation_group_form.py index 5fcd32b..4053060 100644 --- a/arho_feature_template/gui/dialogs/plan_regulation_group_form.py +++ b/arho_feature_template/gui/dialogs/plan_regulation_group_form.py @@ -106,11 +106,7 @@ def __init__(self, regulation_group: RegulationGroup): if self.regulation_group.id_: feat_count = len( - list( - RegulationGroupAssociationLayer.get_associations_for_regulation_group( - str(self.regulation_group.id_) - ) - ) + list(RegulationGroupAssociationLayer.get_associations_for_regulation_group(self.regulation_group.id_)) ) tooltip = ( "Kaavamääräysryhmä on tallennettu kaavaan. Ryhmän tietojen muokkaaminen vaikuttaa " diff --git a/arho_feature_template/project/layers/plan_layers.py b/arho_feature_template/project/layers/plan_layers.py index 9f090da..5c8466b 100644 --- a/arho_feature_template/project/layers/plan_layers.py +++ b/arho_feature_template/project/layers/plan_layers.py @@ -404,13 +404,11 @@ def regulations_with_group_id(cls, group_id: str) -> Generator[QgsFeature]: return cls.get_features_by_attribute_value("plan_regulation_group_id", group_id) @classmethod - def get_regulations_to_delete( - cls, regulations: list[Regulation], regulation_group: RegulationGroup - ) -> list[QgsFeature]: + def get_regulations_to_delete(cls, regulations: list[Regulation], group_id: str) -> list[QgsFeature]: updated_regulation_ids = [regulation.id_ for regulation in regulations] return [ reg - for reg in cls.get_features_by_attribute_value("plan_regulation_group_id", str(regulation_group.id_)) + for reg in cls.get_features_by_attribute_value("plan_regulation_group_id", group_id) if reg["id"] not in updated_regulation_ids ] @@ -457,13 +455,11 @@ def propositions_with_group_id(cls, group_id: str) -> Generator[QgsFeature]: return cls.get_features_by_attribute_value("plan_regulation_group_id", group_id) @classmethod - def get_propositions_to_delete( - cls, propositions: list[Proposition], regulation_group: RegulationGroup - ) -> list[QgsFeature]: + def get_propositions_to_delete(cls, propositions: list[Proposition], group_id: str) -> list[QgsFeature]: updated_proposition_ids = [proposition.id_ for proposition in propositions] return [ prop - for prop in cls.get_features_by_attribute_value("plan_regulation_group_id", str(regulation_group.id_)) + for prop in cls.get_features_by_attribute_value("plan_regulation_group_id", group_id) if prop["id"] not in updated_proposition_ids ] @@ -511,11 +507,11 @@ def model_from_feature(cls, feature: QgsFeature) -> Document: ) @classmethod - def get_documents_to_delete(cls, documents: list[Document], plan: Plan) -> list[QgsFeature]: + def get_documents_to_delete(cls, documents: list[Document], plan_id: str) -> list[QgsFeature]: updated_document_ids = [doc.id_ for doc in documents] return [ doc - for doc in cls.get_features_by_attribute_value("plan_id", str(plan.id_)) + for doc in cls.get_features_by_attribute_value("plan_id", plan_id) if doc["id"] not in updated_document_ids ] From 48cd3975dd97dce0225ebf401a7c59eb3afa013b Mon Sep 17 00:00:00 2001 From: Niko Aarnio Date: Mon, 3 Feb 2025 12:22:56 +0200 Subject: [PATCH 3/5] use get_active_plan_id util function and implement set_active_plan_id util function --- arho_feature_template/core/plan_manager.py | 8 ++++---- arho_feature_template/project/layers/plan_layers.py | 8 ++------ arho_feature_template/utils/misc_utils.py | 5 +++++ 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/arho_feature_template/core/plan_manager.py b/arho_feature_template/core/plan_manager.py index 9cb0db6..0e95423 100644 --- a/arho_feature_template/core/plan_manager.py +++ b/arho_feature_template/core/plan_manager.py @@ -4,7 +4,7 @@ import logging from typing import TYPE_CHECKING -from qgis.core import QgsExpressionContextUtils, QgsProject, QgsVectorLayer, QgsWkbTypes +from qgis.core import QgsProject, QgsVectorLayer, QgsWkbTypes from qgis.gui import QgsMapToolDigitizeFeature from qgis.PyQt.QtWidgets import QDialog, QMessageBox @@ -55,6 +55,7 @@ get_active_plan_id, handle_unsaved_changes, iface, + set_active_plan_id, ) if TYPE_CHECKING: @@ -227,8 +228,7 @@ def edit_plan(self): if not plan_layer: return - active_plan_id = QgsExpressionContextUtils.projectScope(QgsProject.instance()).variable("active_plan_id") - feature = PlanLayer.get_feature_by_id(active_plan_id, no_geometries=False) + feature = PlanLayer.get_feature_by_id(get_active_plan_id(), no_geometries=False) if feature is None: iface.messageBar().pushWarning("", "No active/open plan found!") return @@ -328,7 +328,7 @@ def set_active_plan(self, plan_id: str | None): if previously_in_edit_mode: plan_layer.rollBack() - QgsExpressionContextUtils.setProjectVariable(QgsProject.instance(), "active_plan_id", plan_id) + set_active_plan_id(plan_id) for layer in plan_layers: layer.apply_filter(plan_id) diff --git a/arho_feature_template/project/layers/plan_layers.py b/arho_feature_template/project/layers/plan_layers.py index 5c8466b..564c43d 100644 --- a/arho_feature_template/project/layers/plan_layers.py +++ b/arho_feature_template/project/layers/plan_layers.py @@ -6,7 +6,7 @@ from textwrap import dedent from typing import Any, ClassVar, Generator -from qgis.core import QgsExpressionContextUtils, QgsFeature, QgsProject, QgsVectorLayerUtils +from qgis.core import QgsFeature, QgsVectorLayerUtils from arho_feature_template.core.models import ( AdditionalInformation, @@ -197,11 +197,7 @@ def feature_from_model(cls, model: RegulationGroup, plan_id: str | None = None) feature["short_name"] = model.short_name if model.short_name else None feature["name"] = {LANGUAGE: model.name if model.name else None} feature["type_of_plan_regulation_group_id"] = model.type_code_id - feature["plan_id"] = ( - plan_id - if plan_id - else QgsExpressionContextUtils.projectScope(QgsProject.instance()).variable("active_plan_id") - ) + feature["plan_id"] = plan_id if plan_id else get_active_plan_id() feature["id"] = model.id_ if model.id_ else feature["id"] return feature diff --git a/arho_feature_template/utils/misc_utils.py b/arho_feature_template/utils/misc_utils.py index 00aeaf4..7d6d9bb 100644 --- a/arho_feature_template/utils/misc_utils.py +++ b/arho_feature_template/utils/misc_utils.py @@ -86,6 +86,11 @@ def handle_unsaved_changes() -> bool: return True +def set_active_plan_id(plan_id: str | None): + """Store the given plan ID as the active plan ID as a project variable.""" + QgsExpressionContextUtils.setProjectVariable(QgsProject.instance(), "active_plan_id", plan_id) + + def get_active_plan_id(): """Retrieve the active plan ID stored as a project variable.""" return QgsExpressionContextUtils.projectScope(QgsProject.instance()).variable("active_plan_id") From 95d695dd9ecb795f2962ffe9f69cd6aa0b9449a0 Mon Sep 17 00:00:00 2001 From: Niko Aarnio Date: Mon, 3 Feb 2025 12:26:24 +0200 Subject: [PATCH 4/5] delete unused files and folders, combine exception modules --- arho_feature_template/core/exceptions.py | 2 -- arho_feature_template/exceptions.py | 4 +++ .../gui/dialogs/load_plan_dialog.py | 2 +- arho_feature_template/models/__init__.py | 0 arho_feature_template/models/plan_model_.py | 31 ------------------- arho_feature_template/utils/db_utils.py | 2 +- 6 files changed, 6 insertions(+), 35 deletions(-) delete mode 100644 arho_feature_template/core/exceptions.py delete mode 100644 arho_feature_template/models/__init__.py delete mode 100644 arho_feature_template/models/plan_model_.py diff --git a/arho_feature_template/core/exceptions.py b/arho_feature_template/core/exceptions.py deleted file mode 100644 index 50b61b5..0000000 --- a/arho_feature_template/core/exceptions.py +++ /dev/null @@ -1,2 +0,0 @@ -class UnexpectedNoneError(Exception): - """Internal QGIS errors that should not be happened""" diff --git a/arho_feature_template/exceptions.py b/arho_feature_template/exceptions.py index 7b08566..e719bd8 100644 --- a/arho_feature_template/exceptions.py +++ b/arho_feature_template/exceptions.py @@ -39,3 +39,7 @@ def __init__(self): class FeatureNotFoundError(Exception): def __init__(self, id_: str, layer_name: str): super().__init__(f"Feature with ID '{id_}' not found for layer {layer_name}") + + +class UnexpectedNoneError(Exception): + """Internal QGIS errors that should not be happened""" diff --git a/arho_feature_template/gui/dialogs/load_plan_dialog.py b/arho_feature_template/gui/dialogs/load_plan_dialog.py index de42c6e..cccb279 100644 --- a/arho_feature_template/gui/dialogs/load_plan_dialog.py +++ b/arho_feature_template/gui/dialogs/load_plan_dialog.py @@ -18,7 +18,7 @@ QTableView, ) -from arho_feature_template.core.exceptions import UnexpectedNoneError +from arho_feature_template.exceptions import UnexpectedNoneError from arho_feature_template.utils.misc_utils import get_active_plan_id ui_path = resources.files(__package__) / "load_plan_dialog.ui" diff --git a/arho_feature_template/models/__init__.py b/arho_feature_template/models/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/arho_feature_template/models/plan_model_.py b/arho_feature_template/models/plan_model_.py deleted file mode 100644 index 4ee5134..0000000 --- a/arho_feature_template/models/plan_model_.py +++ /dev/null @@ -1,31 +0,0 @@ -from dataclasses import dataclass -from typing import ClassVar - -from qgis.PyQt.QtCore import QObject, pyqtSignal - - -class SignaledDataClass: - def __setattr__(self, name, value): - if getattr(self, name, None) != value: - signal_name = f"{name}_changed" - if signal_name in self.__class__.__dict__: - signal = getattr(self, signal_name, None) - super().__setattr__(name, value) - if signal: - signal.emit(value) - - -@dataclass -class Plan(QObject, SignaledDataClass): - name_changed: ClassVar = pyqtSignal(str) - - name: str - description: str - is_active: bool = True - - def __post_init__(self): - super().__init__() - - -if __name__ == "__main__": - plan = Plan(name="Basic", description="Basic plan") diff --git a/arho_feature_template/utils/db_utils.py b/arho_feature_template/utils/db_utils.py index b8f67a0..26e2a5c 100644 --- a/arho_feature_template/utils/db_utils.py +++ b/arho_feature_template/utils/db_utils.py @@ -4,7 +4,7 @@ from qgis.core import QgsProviderRegistry -from arho_feature_template.core.exceptions import UnexpectedNoneError +from arho_feature_template.exceptions import UnexpectedNoneError LOGGER = logging.getLogger("LandUsePlugin") From b9855dd08b5f151cb969d610bd7785bd15c51745 Mon Sep 17 00:00:00 2001 From: Niko Aarnio Date: Mon, 3 Feb 2025 12:46:09 +0200 Subject: [PATCH 5/5] misc refactoring --- arho_feature_template/core/plan_manager.py | 6 +++--- arho_feature_template/plugin.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/arho_feature_template/core/plan_manager.py b/arho_feature_template/core/plan_manager.py index 0e95423..e6aec10 100644 --- a/arho_feature_template/core/plan_manager.py +++ b/arho_feature_template/core/plan_manager.py @@ -161,7 +161,7 @@ def edit_regulation_group(self, regulation_group: RegulationGroup): def _open_regulation_group_form(self, regulation_group: RegulationGroup): regulation_group_form = PlanRegulationGroupForm(regulation_group) if regulation_group_form.exec_(): - if regulation_group_form.save_as_config is True: + if regulation_group_form.save_as_config: save_regulation_group_as_config(regulation_group_form.model) else: save_regulation_group(regulation_group_form.model) @@ -191,8 +191,8 @@ def initialize_feature_digitize_map_tool(self, layer: QgsVectorLayer | None = No # Disconnect signals first to not trigger them unwantedly if self.feature_digitize_map_tool: - self.feature_digitize_map_tool.digitizingCompleted.disconnect() - self.feature_digitize_map_tool.digitizingFinished.disconnect() + disconnect_signal(self.feature_digitize_map_tool.digitizingCompleted) + disconnect_signal(self.feature_digitize_map_tool.digitizingFinished) # Reinitialize and reconnect signals self.feature_digitize_map_tool = PlanFeatureDigitizeMapTool(mode) diff --git a/arho_feature_template/plugin.py b/arho_feature_template/plugin.py index ab991cd..760deff 100644 --- a/arho_feature_template/plugin.py +++ b/arho_feature_template/plugin.py @@ -13,7 +13,7 @@ from arho_feature_template.qgis_plugin_tools.tools.custom_logging import setup_logger, teardown_logger from arho_feature_template.qgis_plugin_tools.tools.i18n import setup_translation from arho_feature_template.qgis_plugin_tools.tools.resources import plugin_name, resources_path -from arho_feature_template.utils.misc_utils import iface +from arho_feature_template.utils.misc_utils import disconnect_signal, iface class Plugin: @@ -257,7 +257,7 @@ def open_settings(self): def unload(self) -> None: """Removes the plugin menu item and icon from QGIS GUI.""" # Handle signals - self.plan_manager.new_feature_dock.visibilityChanged.disconnect() + disconnect_signal(self.plan_manager.new_feature_dock.visibilityChanged) iface.mapCanvas().mapToolSet.disconnect() iface.projectRead.disconnect()