diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e80f11c..c91b669 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,14 +20,14 @@ on: jobs: - flake8: + ruff: runs-on: ubuntu-latest steps: - - name: Setup Python 3.9 + - name: Setup Python 3.12 uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: 3.12 architecture: x64 - name: Checkout @@ -43,8 +43,11 @@ jobs: - name: Install Python requirements run: pip install -r requirements/dev.txt - - name: Run flake8 - run: flake8 + - name: Run ruff linter + run: ruff check + + - name: Run ruff formatter + run: ruff format --check tests-unit: runs-on: ubuntu-latest @@ -73,7 +76,7 @@ jobs: run: pytest tests/unit/ release: - needs: [flake8, tests-unit] + needs: [ruff, tests-unit] runs-on: ubuntu-latest if: github.repository_owner == 'Artelia' && contains(github.ref, 'refs/tags/') diff --git a/.gitignore b/.gitignore index e3403eb..464593d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,8 @@ *.pyc .idea/workspace.xml .idea/* -.vscode/* __pycache__ *.qm *.zip .env +.venv diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..083f6e5 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,37 @@ +exclude: ".venv|__pycache__|tests/dev/|tests/fixtures/" + +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: check-added-large-files + args: + - --maxkb=500 + - id: check-case-conflict + - id: check-toml + - id: check-xml + - id: check-yaml + - id: detect-private-key + - id: end-of-file-fixer + - id: fix-byte-order-marker + - id: trailing-whitespace + args: + - --markdown-linebreak-ext=md + +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: "v0.12.12" + hooks: + - id: ruff + args: + - --fix + - --target-version=py312 + types_or: + - python + - pyi + - id: ruff-format + args: + - --line-length=120 + - --target-version=py312 + types_or: + - python + - pyi diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..1459a75 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,41 @@ +{ + // Editor + "editor.bracketPairColorization.enabled": true, + "editor.guides.bracketPairs": "active", + "files.associations": { + "./requirements/*.txt": "pip-requirements", + "metadata.txt": "ini", + "**/*.model3": "xml", + "**/*.ts": "xml", + "**/*.ui": "xml" + }, + // Python + "python.analysis.autoFormatStrings": false, // breaking PyQt translation + "python.analysis.typeCheckingMode": "off", + "python.terminal.activateEnvInCurrentTerminal": true, + "python.terminal.activateEnvironment": true, + "[python]": { + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports.ruff": "explicit", + "source.fixAll.ruff": "explicit" + }, + "editor.rulers": [ + 120 + ], + "editor.wordWrapColumn": 120, + }, + + // Ruff + "ruff.importStrategy": "fromEnvironment", + "ruff.configuration": "./pyproject.toml", + + // Tests + "python.testing.unittestEnabled": true, + "python.testing.pytestEnabled": true, + // Extensions + "autoDocstring.docstringFormat": "sphinx", + "python-envs.defaultEnvManager": "ms-python.python:venv", + "python-envs.pythonProjects": [], +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..d25d34e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,20 @@ +# Contributing Guidelines + +First off, thanks for considering to contribute to this project! + +These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. + +## Git hooks + +We use git hooks through [pre-commit](https://pre-commit.com/) to enforce and automatically check some "rules". Please install them (`pre-commit install`) before to push any commit. + +See the relevant configuration file: `.pre-commit-config.yaml`. + +## Code Style + +Make sure your code *roughly* follows [PEP-8](https://www.python.org/dev/peps/pep-0008/) and keeps things consistent with the rest of the code: + +- docstrings: [sphinx-style](https://sphinx-rtd-tutorial.readthedocs.io/en/latest/docstrings.html#the-sphinx-docstring-format) is used to write technical documentation. +- formatting: [ruff formatter](https://docs.astral.sh/ruff/formatter/) is used to automatically format the code without debate. +- sorted imports: [ruff linter](https://docs.astral.sh/ruff/linter/) is used to sort imports +- static analisis: [ruff linter](https://docs.astral.sh/ruff/linter/) is used to catch some dizziness and keep the source code healthy. diff --git a/README.md b/README.md index ab42246..309dbc0 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,25 @@ -# Mesh Tools +# Mesh Tools + Tools for management of data using Mesh format for Telemac or Uhaina More informations on Telemac could be found on [Open Telemac-Mascaret website]("http://www.opentelemac.org/") More informations on Uhaina could be found (soon) ## Installation + Use the QGIS Plugins menu to install the Mesh Tools Plugin ## Development -Some makefile methods require to install few dependecies. -For translations `make transup` and `make transcompile`: +This project is configured with the following tools: -```bash -apt install qttools5-dev-tools pyqt5-dev-tools -``` +- [ruff](https://docs.astral.sh/ruff/) to format the code without any existential question and sort import -For code formating `make black`: +Code rules are enforced with [pre-commit](https://pre-commit.com/) hooks. +Static code analisis is based on: Flake8 -```bash -pip install black isort -``` +See also: [contribution guidelines](CONTRIBUTING.md). ## License + Mesh_Tools plugin is free software. You can redistribute it anf/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation. diff --git a/help/source/conf.py b/help/source/conf.py index 19611b3..cdb3633 100644 --- a/help/source/conf.py +++ b/help/source/conf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # MeshTools documentation build configuration file, created by # sphinx-quickstart on Sun Feb 12 17:11:03 2012. @@ -11,8 +10,6 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import os -import sys # 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 diff --git a/mesh_tools/__about__.py b/mesh_tools/__about__.py index f0e5c00..a1ad11d 100644 --- a/mesh_tools/__about__.py +++ b/mesh_tools/__about__.py @@ -1,8 +1,8 @@ #! python3 # noqa: E265 """ - Metadata about the package to easily retrieve informations about it. - See: https://packaging.python.org/guides/single-sourcing-package-version/ +Metadata about the package to easily retrieve informations about it. +See: https://packaging.python.org/guides/single-sourcing-package-version/ """ # ############################################################################ @@ -50,7 +50,7 @@ def plugin_metadata_as_dict() -> dict: if PLG_METADATA_FILE.is_file(): config.read(PLG_METADATA_FILE.resolve(), encoding="UTF-8") return {s: dict(config.items(s)) for s in config.sections()} - raise IOError(f"Plugin metadata.txt not found at: {PLG_METADATA_FILE}") + raise OSError(f"Plugin metadata.txt not found at: {PLG_METADATA_FILE}") # ############################################################################ @@ -65,7 +65,7 @@ def plugin_metadata_as_dict() -> dict: __email__ = __plugin_md__.get("general").get("email") __keywords__ = [t.strip() for t in __plugin_md__.get("general").get("repository").split("tags")] __license__ = "GPLv3" -__summary__ = f'{__plugin_md__.get("general").get("description")}\n{__plugin_md__.get("general").get("about")}' +__summary__ = f"{__plugin_md__.get('general').get('description')}\n{__plugin_md__.get('general').get('about')}" __title__ = __plugin_md__.get("general").get("name") __title_clean__ = "".join(e for e in __title__ if e.isalnum()) @@ -91,6 +91,6 @@ def plugin_metadata_as_dict() -> dict: print("Version : " + __version__) print("Description : " + __summary__) print( - f'Pour : {plugin_md.get("general").get("qgisminimumversion")} < QGIS < {plugin_md.get("general").get("qgismaximumversion", "3.99")}' + f"Pour : {plugin_md.get('general').get('qgisminimumversion')} < QGIS < {plugin_md.get('general').get('qgismaximumversion', '3.99')}" ) print(__title_clean__) diff --git a/mesh_tools/__init__.py b/mesh_tools/__init__.py index 2f42a35..016ca3c 100644 --- a/mesh_tools/__init__.py +++ b/mesh_tools/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ /*************************************************************************** MeshTools diff --git a/mesh_tools/libs/MeshUtils.py b/mesh_tools/libs/MeshUtils.py index 5842091..3858408 100644 --- a/mesh_tools/libs/MeshUtils.py +++ b/mesh_tools/libs/MeshUtils.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """ /*************************************************************************** MeshUtils @@ -22,9 +20,8 @@ * * ***************************************************************************/ """ -import functools -from typing import Optional +import functools from qgis.core import ( QgsCoordinateTransform, @@ -120,14 +117,14 @@ def xyFromN(nativeMesh: QgsMesh, n: int, mesh_xorm: QgsCoordinateTransform, laye return point, error @staticmethod - def createVerticesSpatialIndex(nativeMesh: QgsMesh, xform: Optional[QgsCoordinateTransform] = None): + def createVerticesSpatialIndex(nativeMesh: QgsMesh, xform: QgsCoordinateTransform | None = None): spindex = QgsSpatialIndex() count = nativeMesh.vertexCount() offset = 0 batch_size = 10 while offset < count: - lst_ft = list() + lst_ft = [] iterations = min(batch_size, count - offset) for i in range(iterations): ft = QgsFeature() @@ -158,7 +155,7 @@ def computeNeighbors(nativeMesh: QgsMesh) -> dict: for vertex in verticies: neighbors = list(verticies) neighbors.remove(vertex) - if vertex in dico.keys(): + if vertex in dico: for neighbor in neighbors: if neighbor not in dico[vertex]: dico[vertex].append(neighbor) diff --git a/mesh_tools/libs/__init__.py b/mesh_tools/libs/__init__.py index c21624a..e69de29 100644 --- a/mesh_tools/libs/__init__.py +++ b/mesh_tools/libs/__init__.py @@ -1,37 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -/*************************************************************************** - classFactory - A QGIS plugin - Tools for management of Data on mesh (Telemac, Uhaina) - Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/ - ------------------- - begin : 2021-03-24 - git sha : $Format:%H$ - copyright : (C) 2021 by Artelia/BRGM/ISL - email : a@a - ***************************************************************************/ - -/*************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ -""" - - -# noinspection PyPep8Naming -def classFactory(iface): # pylint: disable=invalid-name - """Load MeshTools class from file MeshTools. - - :param iface: A QGIS interface instance. - :type iface: QgsInterface - """ - # - from .mesh_tools import MeshTools - - return MeshTools(iface) diff --git a/mesh_tools/libs/create_shp_dlg.py b/mesh_tools/libs/create_shp_dlg.py index 1abe21f..a97982f 100644 --- a/mesh_tools/libs/create_shp_dlg.py +++ b/mesh_tools/libs/create_shp_dlg.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """ /*************************************************************************** dlg_create_culvert_shapefile @@ -35,7 +33,7 @@ class dlg_create_shapefile(QDialog, FORM_CLASS): def __init__(self, name, crs_mesh=None, parent=None): - super(dlg_create_shapefile, self).__init__() + super().__init__() self.setupUi(self) self.tr = parent.tr self.setWindowTitle(self.tr("New {} layer", self.__class__.__name__).format(name)) diff --git a/mesh_tools/libs/culvert_manager.py b/mesh_tools/libs/culvert_manager.py index 89442c5..6821541 100644 --- a/mesh_tools/libs/culvert_manager.py +++ b/mesh_tools/libs/culvert_manager.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """ /*************************************************************************** CulvertManager @@ -25,7 +23,6 @@ import os import time - from contextlib import suppress import numpy as np @@ -65,16 +62,14 @@ from .import_culvert_file_dlg import dlg_import_culvert_file from .MeshUtils import MeshUtils -FORM_CLASS, _ = uic.loadUiType( - os.path.join(os.path.dirname(__file__), "..", "ui", "culvert_manager.ui") -) +FORM_CLASS, _ = uic.loadUiType(os.path.join(os.path.dirname(__file__), "..", "ui", "culvert_manager.ui")) class CulvertManager(MeshToolsDockWidget, FORM_CLASS): closingTool = pyqtSignal() def __init__(self, parent=None): - super(CulvertManager, self).__init__(parent) + super().__init__(parent) self.setupUi(self) self.prt = parent self.path_icon = os.path.join(os.path.dirname(__file__), "..", "icons/") @@ -130,7 +125,7 @@ def __init__(self, parent=None): ["AA", QVariant.Int, self.cb_auto_a, 25, 13 ], ["NB_in_//", QVariant.Int, self.sb_nbre, None, 21 ], ["AL", QVariant.Int, self.cb_auto_l, 26, None], - ["AZ", QVariant.Int, self.cb_auto_z, 27, 22 ] + ["AZ", QVariant.Int, self.cb_auto_z, 27, 22 ], ] # fmt: on @@ -168,7 +163,7 @@ def __init__(self, parent=None): for fld in self.culv_flds: ctrl = fld[2] if ctrl: - if isinstance(ctrl, QDoubleSpinBox) or isinstance(ctrl, QSpinBox): + if isinstance(ctrl, QDoubleSpinBox | QSpinBox): ctrl.valueChanged.connect(self.ctrl_edited) elif isinstance(ctrl, QComboBox): ctrl.currentIndexChanged.connect(self.ctrl_edited) @@ -295,9 +290,7 @@ def cur_mesh_changed(self): self.writeInfo(self.tr("Creation of vertices spatial index...")) t0 = time.time() self.vertices = MeshUtils.createVerticesSpatialIndex(self.native_mesh, self.lay_mesh_xform) - self.writeInfo( - self.tr("Vertices spatial index created in {} sec.").format(round(time.time() - t0, 1)) - ) + self.writeInfo(self.tr("Vertices spatial index created in {} sec.").format(round(time.time() - t0, 1))) self.cb_dataset_mesh.blockSignals(True) mesh_prov = self.lay_mesh.dataProvider() @@ -322,11 +315,7 @@ def mesh_dataset_changed(self): self.mdl_mesh_time.clear() self.cur_mesh_dataset = self.cb_dataset_mesh.currentData(32) if self.cur_mesh_dataset is not None: - self.writeInfo( - self.tr("Current mesh dataset changed : {}").format( - self.cb_dataset_mesh.currentText() - ) - ) + self.writeInfo(self.tr("Current mesh dataset changed : {}").format(self.cb_dataset_mesh.currentText())) mesh_prov = self.lay_mesh.dataProvider() for i in range(mesh_prov.datasetCount(self.cur_mesh_dataset)): itm = QStandardItem() @@ -344,11 +333,7 @@ def mesh_dataset_changed(self): def mesh_time_changed(self): self.cur_mesh_time = self.cb_time_mesh.currentData(32) if self.cur_mesh_time is not None: - self.writeInfo( - self.tr("Current mesh timestep changed : {}").format( - self.cb_time_mesh.currentText() - ) - ) + self.writeInfo(self.tr("Current mesh timestep changed : {}").format(self.cb_time_mesh.currentText())) if self.lay_culv is not None and not self.is_opening: self.update_all_n() if ( @@ -356,8 +341,7 @@ def mesh_time_changed(self): self, self.tr("Automatic Z Update"), self.tr( - "Mesh parameters have been changed.\n" - "Update culvert features with Automatic Z checked ?" + "Mesh parameters have been changed.\nUpdate culvert features with Automatic Z checked ?" ), QMessageBox.Cancel | QMessageBox.Ok, ) @@ -439,10 +423,7 @@ def culv_lay_changed(self): QMessageBox.question( self, self.tr("Automatic Z Update"), - self.tr( - "Culvert layer has been changed.\n" - "Update culvert features with Automatic Z checked ?" - ), + self.tr("Culvert layer has been changed.\nUpdate culvert features with Automatic Z checked ?"), QMessageBox.Cancel | QMessageBox.Ok, ) == QMessageBox.Ok @@ -533,50 +514,42 @@ def display_info(self, ctrl, val): def ctrl_edited(self): ft = None - if not self.ctrl_signal_blocked: - if self.cur_culv_id is not None: - ft = self.lay_culv.getFeature(self.cur_culv_id) - - ctrl = self.sender() - if isinstance(ctrl, QDoubleSpinBox) or isinstance(ctrl, QSpinBox): - val = ctrl.value() - elif isinstance(ctrl, QComboBox): - val = ctrl.currentIndex() - elif isinstance(ctrl, QCheckBox): - if ctrl.checkState() == 2: - val = 1 - else: - val = 0 - elif isinstance(ctrl, QLineEdit): - val = ctrl.text() - - field_idx = None - for idx in range(len(self.culv_flds)): - if ctrl == self.culv_flds[idx][2]: - field_name = self.culv_flds[idx][0] - field_idx = self.lay_culv.fields().indexFromName(field_name) - break - - if field_idx is not None: - attrs = {field_idx: val} - self.lay_culv.dataProvider().changeAttributeValues({self.cur_culv_id: attrs}) - self.lay_culv.commitChanges() + if not self.ctrl_signal_blocked and self.cur_culv_id is not None: + ft = self.lay_culv.getFeature(self.cur_culv_id) + + ctrl = self.sender() + if isinstance(ctrl, QDoubleSpinBox | QSpinBox): + val = ctrl.value() + elif isinstance(ctrl, QComboBox): + val = ctrl.currentIndex() + elif isinstance(ctrl, QCheckBox): + val = 1 if ctrl.checkState() == 2 else 0 + elif isinstance(ctrl, QLineEdit): + val = ctrl.text() + + field_idx = None + for idx in range(len(self.culv_flds)): + if ctrl == self.culv_flds[idx][2]: + field_name = self.culv_flds[idx][0] + field_idx = self.lay_culv.fields().indexFromName(field_name) + break + + if field_idx is not None: + attrs = {field_idx: val} + self.lay_culv.dataProvider().changeAttributeValues({self.cur_culv_id: attrs}) + self.lay_culv.commitChanges() if self.sender() == self.cb_auto_z: self.sb_z1.setEnabled(not self.cb_auto_z.isChecked()) self.sb_z2.setEnabled(not self.cb_auto_z.isChecked()) if ft: - (n1, n2), err = MeshUtils.n1n2FromFeature( - self.lay_mesh, self.vertices, ft, self.lay_culv_xform - ) + (n1, n2), err = MeshUtils.n1n2FromFeature(self.lay_mesh, self.vertices, ft, self.lay_culv_xform) if err is not None: self.writeError(self.tr("Error on Z calculation : {}").format(err)) return - attrs = { - self.cur_culv_id: {ft.fieldNameIndex("N1"): n1, ft.fieldNameIndex("N2"): n2} - } + attrs = {self.cur_culv_id: {ft.fieldNameIndex("N1"): n1, ft.fieldNameIndex("N2"): n2}} if self.cb_auto_z.isChecked(): z1 = MeshUtils.zFromN( @@ -637,12 +610,10 @@ def reset_val(self): ctrl.setCheckState(0) def update_all_n(self, log=True): - attrs = dict() + attrs = {} success = True for ft in self.lay_culv.getFeatures(): - (n1, n2), err = MeshUtils.n1n2FromFeature( - self.lay_mesh, self.vertices, ft, self.lay_culv_xform - ) + (n1, n2), err = MeshUtils.n1n2FromFeature(self.lay_mesh, self.vertices, ft, self.lay_culv_xform) if err is not None: success = False @@ -659,14 +630,12 @@ def update_all_n(self, log=True): self.writeSuccess(self.tr("N values updated")) def update_all_auto_z(self): - attrs = dict() + attrs = {} for ft in self.lay_culv.getFeatures(): if ft["AZ"] != 1: continue - (n1, n2), err = MeshUtils.n1n2FromFeature( - self.lay_mesh, self.vertices, ft, self.lay_culv_xform - ) + (n1, n2), err = MeshUtils.n1n2FromFeature(self.lay_mesh, self.vertices, ft, self.lay_culv_xform) if err is None: z1 = MeshUtils.zFromN( self.lay_mesh, @@ -715,9 +684,7 @@ def create_file(self): self.writeError(self.tr("File creation is not possible, some culverts are not valid")) return - culv_file_name, _ = QFileDialog.getSaveFileName( - self, self.tr("Culvert file"), "", self.tr("Text File (*.txt)") - ) + culv_file_name, _ = QFileDialog.getSaveFileName(self, self.tr("Culvert file"), "", self.tr("Text File (*.txt)")) if not culv_file_name: return @@ -727,12 +694,12 @@ def create_file(self): with open(culv_file_name, "w", encoding="utf-8") as culv_file: if self.software_select == 0: # Telemac - culv_file.write("Relaxation" + str("\t") + self.tr("Culvert count") + str("\n")) - culv_file.write(str(relax) + str("\t") + str(nb_culv) + str("\n")) + culv_file.write("Relaxation" + "\t" + self.tr("Culvert count") + "\n") + culv_file.write(str(relax) + "\t" + str(nb_culv) + "\n") idx_out = 3 elif self.software_select == 1: # Uhaina - culv_file.write(self.tr("Culvert count") + str("\n")) - culv_file.write(str(nb_culv) + str("\n")) + culv_file.write(self.tr("Culvert count") + "\n") + culv_file.write(str(nb_culv) + "\n") idx_out = 4 culv_flds_srtd = [ @@ -800,22 +767,21 @@ def verif_culvert_validity(self): selectedids.append([ft_name, self.tr("Culvert extremity is not within the mesh.")]) for fld in self.culv_flds: - if fld[0] not in ["NAME", "Remarques"] and fld[2]: - if fld[1] == QVariant.String: - if (ft[fld[0]] == NULL) or not isinstance(ft[fld[0]], str): - selectedids.append( - [ft_name, self.tr("{} value is not correct.").format(fld[0])] - ) - elif fld[1] == QVariant.Double: - if (ft[fld[0]] == NULL) or not isinstance(ft[fld[0]], float): - selectedids.append( - [ft_name, self.tr("{} value is not correct.").format(fld[0])] - ) - elif fld[1] == QVariant.Int: - if (ft[fld[0]] == NULL) or not isinstance(ft[fld[0]], int): - selectedids.append( - [ft_name, self.tr("{} value is not correct.").format(fld[0])] - ) + if ( + fld[0] not in ["NAME", "Remarques"] + and fld[2] + and ( + fld[1] == QVariant.String + and ((ft[fld[0]] == NULL) or not isinstance(ft[fld[0]], str)) + or ( + fld[1] == QVariant.Double + and ((ft[fld[0]] == NULL) or not isinstance(ft[fld[0]], float)) + or fld[1] == QVariant.Int + and ((ft[fld[0]] == NULL) or not isinstance(ft[fld[0]], int)) + ) + ) + ): + selectedids.append([ft_name, self.tr("{} value is not correct.").format(fld[0])]) return selectedids @@ -849,10 +815,7 @@ def asDict(headers, values): self.writeError(self.tr("Import a mesh first.")) return - if self.lay_mesh.crs().isValid(): - mesh_crs = self.lay_mesh.crs() - else: - mesh_crs = self.project.crs() + mesh_crs = self.lay_mesh.crs() if self.lay_mesh.crs().isValid() else self.project.crs() culv_flds = [x[0] for x in self.culv_flds] dlg = dlg_import_culvert_file(culv_flds, mesh_crs, self) @@ -863,9 +826,7 @@ def asDict(headers, values): txt_path = dlg.text_file.filePath() layer_path = dlg.layer_file.filePath() layer_crs = dlg.layer_crs.crs() - culv_xform = QgsCoordinateTransform( - layer_crs, self.canvas.mapSettings().destinationCrs(), self.project - ) + culv_xform = QgsCoordinateTransform(layer_crs, self.canvas.mapSettings().destinationCrs(), self.project) layerDriver = GdalUtils.getVectorDriverFromFileName(layer_path) else: return @@ -877,15 +838,13 @@ def asDict(headers, values): for fld in self.culv_flds: layerFields.append(QgsField(fld[0], fld[1])) - layer = QgsVectorLayer( - f"MultiLineString?crs={mesh_crs.authid()}", self.tr("Culverts"), "memory" - ) + layer = QgsVectorLayer(f"MultiLineString?crs={mesh_crs.authid()}", self.tr("Culverts"), "memory") pr = layer.dataProvider() pr.addAttributes(layerFields) layer.updateFields() layer.startEditing() - with open(txt_path, "r") as txt_file: + with open(txt_path) as txt_file: # 1st line is comment txt_file.readline() # Relaxation and number of culverts @@ -912,19 +871,13 @@ def asDict(headers, values): n1 = int(values[items["n1"][1]]) n2 = int(values[items["n2"][1]]) - point_n1, err1 = MeshUtils.xyFromN( - self.native_mesh, n1, self.lay_mesh_xform, culv_xform - ) - point_n2, err2 = MeshUtils.xyFromN( - self.native_mesh, n2, self.lay_mesh_xform, culv_xform - ) + point_n1, err1 = MeshUtils.xyFromN(self.native_mesh, n1, self.lay_mesh_xform, culv_xform) + point_n2, err2 = MeshUtils.xyFromN(self.native_mesh, n2, self.lay_mesh_xform, culv_xform) if err1 is not None or err2 is not None: err = " ".join(filter(None, (err1, err2))) self.writeError( - self.tr("Error when importing culvert {i} with error(s) : {err}").format( - i=i + 1, err=err - ) + self.tr("Error when importing culvert {i} with error(s) : {err}").format(i=i + 1, err=err) ) continue @@ -934,13 +887,7 @@ def asDict(headers, values): attrs = [] for fld in self.culv_flds: key = items[fld[0].lower()][1] - if key: - attr = values[key] - else: - if fld[1] in [QVariant.Int, QVariant.Double]: - attr = 0 - else: - attr = "" + attr = values[key] if key else 0 if fld[1] in [QVariant.Int, QVariant.Double] else "" attrs.append(attr) fet.setAttributes(attrs) @@ -962,9 +909,7 @@ def asDict(headers, values): transformContext=QgsCoordinateTransformContext(), options=options, ) - layer = QgsVectorLayer( - layer_path, os.path.basename(layer_path).rsplit(".", 1)[0], "ogr" - ) + layer = QgsVectorLayer(layer_path, os.path.basename(layer_path).rsplit(".", 1)[0], "ogr") if layer.isValid(): layer.setCrs(layer_crs) diff --git a/mesh_tools/libs/dialog_for_CAS.py b/mesh_tools/libs/dialog_for_CAS.py index ec3065b..2da0098 100644 --- a/mesh_tools/libs/dialog_for_CAS.py +++ b/mesh_tools/libs/dialog_for_CAS.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """ /*************************************************************************** DialogForCAS @@ -22,6 +20,7 @@ * * ***************************************************************************/ """ + import os from qgis.PyQt import uic @@ -33,7 +32,7 @@ class DialogForCAS(QDialog, FORM_CLASS): def __init__(self, parent=None): - super(DialogForCAS, self).__init__() + super().__init__() self.setupUi(self) font = QFont() diff --git a/mesh_tools/libs/import_culvert_file_dlg.py b/mesh_tools/libs/import_culvert_file_dlg.py index cf7ded4..66bb4fb 100644 --- a/mesh_tools/libs/import_culvert_file_dlg.py +++ b/mesh_tools/libs/import_culvert_file_dlg.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """ /*************************************************************************** dlg_import_culvert_file @@ -30,14 +28,12 @@ from qgis.PyQt.QtCore import QCoreApplication from qgis.PyQt.QtWidgets import QComboBox, QDialog, QTableWidgetItem -FORM_CLASS, _ = uic.loadUiType( - os.path.join(os.path.dirname(__file__), "..", "ui", "import_culvert_file.ui") -) +FORM_CLASS, _ = uic.loadUiType(os.path.join(os.path.dirname(__file__), "..", "ui", "import_culvert_file.ui")) class dlg_import_culvert_file(QDialog, FORM_CLASS): def __init__(self, culv_flds=None, mesh_crs=None, parent=None): - super(dlg_import_culvert_file, self).__init__() + super().__init__() self.setupUi(self) self.items = {} @@ -122,7 +118,7 @@ def get_values(string): self.updateTable() return - with open(path, "r") as txt_file: + with open(path) as txt_file: # First line is always a comment txt_file.readline() txt_file.readline() @@ -130,7 +126,7 @@ def get_values(string): for header in headers: key = None - if header.lower() in self.items.keys(): + if header.lower() in self.items: key = header.lower() elif header.lower() in ["i1", "i2"]: key = f"n{header.lower()[1]}" diff --git a/mesh_tools/libs/mesh_quality.py b/mesh_tools/libs/mesh_quality.py index 57ae814..7d70fa4 100644 --- a/mesh_tools/libs/mesh_quality.py +++ b/mesh_tools/libs/mesh_quality.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """ /*************************************************************************** MeshQuality @@ -39,7 +37,7 @@ class MeshQuality(MeshToolsDockWidget, FORM_CLASS): def __init__(self, parent=None): - super(MeshQuality, self).__init__(parent) + super().__init__(parent) self.setupUi(self) self.prt = parent self.path_icon = os.path.join(os.path.dirname(__file__), "..", "icons/") diff --git a/mesh_tools/libs/source_manager.py b/mesh_tools/libs/source_manager.py index 90d6245..da225cf 100644 --- a/mesh_tools/libs/source_manager.py +++ b/mesh_tools/libs/source_manager.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """ /*************************************************************************** SourceManager @@ -25,7 +23,6 @@ import os import time - from contextlib import suppress from processing.algs.gdal.GdalUtils import GdalUtils @@ -61,7 +58,7 @@ class SourceManager(MeshToolsDockWidget, FORM_CLASS): closingTool = pyqtSignal() def __init__(self, parent=None): - super(SourceManager, self).__init__(parent) + super().__init__(parent) self.setupUi(self) self.prt = parent self.path_icon = os.path.join(os.path.dirname(__file__), "..", "icons/") @@ -129,7 +126,7 @@ def analyse_project_layers(self): self.analyse_layer(lay) def analyse_layer(self, lay): - if not lay.type() == QgsMapLayerType.VectorLayer: + if lay.type() != QgsMapLayerType.VectorLayer: return if lay.geometryType() != self.src_type: @@ -357,22 +354,21 @@ def fill_info(self): self.ctrl_signal_blocked = False def ctrl_edited(self): - if not self.ctrl_signal_blocked: - if self.cur_src_id is not None: - ctrl = self.sender() - val = ctrl.text() - - field_idx = None - for idx in range(len(self.src_flds)): - if ctrl == self.src_flds[idx][2]: - field_name = self.src_flds[idx][0] - field_idx = self.lay_src.fields().indexFromName(field_name) - break - - if field_idx is not None: - attrs = {field_idx: val} - self.lay_src.dataProvider().changeAttributeValues({self.cur_src_id: attrs}) - self.lay_src.commitChanges() + if not self.ctrl_signal_blocked and self.cur_src_id is not None: + ctrl = self.sender() + val = ctrl.text() + + field_idx = None + for idx in range(len(self.src_flds)): + if ctrl == self.src_flds[idx][2]: + field_name = self.src_flds[idx][0] + field_idx = self.lay_src.fields().indexFromName(field_name) + break + + if field_idx is not None: + attrs = {field_idx: val} + self.lay_src.dataProvider().changeAttributeValues({self.cur_src_id: attrs}) + self.lay_src.commitChanges() def clear_hl_vertices(self): for marker in self.hl_vertices: @@ -458,10 +454,7 @@ def export(self): for feat in self.lay_src.getFeatures(): featId = feat.id() + 1 src_file.write("#\n") - if feat[nameField] != NULL: - scr_name = feat[nameField] - else: - scr_name = self.tr("SOURCE REGION {}").format(featId) + scr_name = feat[nameField] if feat[nameField] != NULL else self.tr("SOURCE REGION {}").format(featId) src_file.write("{} {}\n".format(self.tr("# COORDINATES AT"), scr_name)) src_file.write("#\n") src_file.write(f"X({featId})\tY({featId})\n") diff --git a/mesh_tools/mesh_tools.py b/mesh_tools/mesh_tools.py index 9e29d9a..db6ab43 100644 --- a/mesh_tools/mesh_tools.py +++ b/mesh_tools/mesh_tools.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """ /*************************************************************************** MeshTools @@ -57,7 +55,7 @@ def __init__(self): # initialize locale locale = QSettings().value("locale/userLocale")[0:2] - locale_path = os.path.join(self.plugin_dir, "i18n", "MeshTools_{}.qm".format(locale)) + locale_path = os.path.join(self.plugin_dir, "i18n", f"MeshTools_{locale}.qm") if os.path.exists(locale_path): self.translator = QTranslator() diff --git a/mesh_tools/mesh_tools_dockwidget.py b/mesh_tools/mesh_tools_dockwidget.py index 32481df..0138eb8 100644 --- a/mesh_tools/mesh_tools_dockwidget.py +++ b/mesh_tools/mesh_tools_dockwidget.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """ /*************************************************************************** MeshToolsDockWidget @@ -37,7 +35,7 @@ class MeshToolsDockWidget(QDockWidget): closingPlugin = pyqtSignal() def __init__(self, parent=None): - super(MeshToolsDockWidget, self).__init__(parent) + super().__init__(parent) self.iface = iface self.canvas = self.iface.mapCanvas() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..732a9a8 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,77 @@ +[tool.qgis-plugin-ci] +plugin_path = "mesh_tools" +github_organization_slug = "Artelia" +project_slug = "mesh_tools" + +# Configuration pytest (migrée depuis setup.cfg) +[tool.pytest.ini_options] +addopts = [ + "--junitxml=junit/test-results.xml", + "--cov-config=pyproject.toml", + "--cov=processing_swan_provider", + "--cov-report=html", + "--cov-report=term", + "--cov-report=xml", + "--ignore=tests/_wip/" + ] +norecursedirs = ".* build dev development dist docs CVS fixtures _darcs {arch} *.egg venv _wip" +python_files = "test_*.py" +testpaths = ["tests"] + +# Configuration coverage (migrée depuis setup.cfg) +[tool.coverage.run] +branch = true +omit = [ + ".venv/*", + "*tests*" + ] + +[tool.coverage.report] +exclude_lines = [ + "if self.debug:", + "pragma: no cover", + "raise NotImplementedError", + "if __name__ == .__main__.:" + ] +ignore_errors = true +show_missing = true + +[tool.ruff] +line-length = 120 +target-version = "py312" + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "auto" + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "UP", # pyupgrade + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "PIE", # flake8-pie + "SIM", # flake8-simplify + ] +ignore = [ + "E203", # whitespace before ':' (conflicts with black) + "E501", # line too long (handled by formatter) + ] + +[tool.ruff.lint.isort] +known-first-party = ["mesh_tools"] +section-order = ["future", "standard-library", "third-party", "qgis", "first-party", "local-folder"] +split-on-trailing-comma = true +force-single-line = false +force-sort-within-sections = false + +[tool.ruff.lint.isort.sections] +qgis = ["qgis", "processing"] + +[tool.ruff.lint.per-file-ignores] +"tests/**" = ["S101"] # Allow assert in tests diff --git a/requirements/dev.txt b/requirements/dev.txt index d6fb279..7e7ef78 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,5 +1,5 @@ -flake8 -flake8-builtins -flake8-print -flake8-qgis -isort +pyqt5-stubs + +ruff + +pre-commit>=4,<5 diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 41ef617..0000000 --- a/setup.cfg +++ /dev/null @@ -1,38 +0,0 @@ -[qgis-plugin-ci] -plugin_path = mesh_tools -github_organization_slug = Artelia -project_slug = mesh_tools -transifex_organization = mesh-tools -transifex_project = mesh-tool-plugin -transifex_resource = MeshTools -transifex_coordinator = nicogodet - -[isort] -multi_line_output = 3 -include_trailing_comma = True -use_parentheses = True -ensure_newline_before_comments = True -lines_between_types = 1 - -[flake8] -max-line-length = 120 -ignore = - # E123 closing bracket does not match indentation of opening bracket's line - E123, - E800, - W503, - W504, - W605, - -per-file-ignores = - mesh_tools/libs/culvert_manager.py: E202, E241 - -exclude = - .git, - __pycache__, - .github/, - help/, - .venv/, - venv/, - mesh_tools/__about__.py, - tests/qgis/runner.py diff --git a/tests/qgis/runner.py b/tests/qgis/runner.py index db1a445..0021677 100644 --- a/tests/qgis/runner.py +++ b/tests/qgis/runner.py @@ -7,17 +7,18 @@ import unittest from osgeo import gdal + from qgis.core import Qgis from qgis.PyQt import Qt def pytest_report_header(config): """Used by PyTest and Unittest.""" - message = "QGIS : {}\n".format(Qgis.QGIS_VERSION_INT) + message = f"QGIS : {Qgis.QGIS_VERSION_INT}\n" message += "Python GDAL : {}\n".format(gdal.VersionInfo("VERSION_NUM")) - message += "Python : {}\n".format(sys.version) + message += f"Python : {sys.version}\n" # message += 'Python path : {}'.format(sys.path) - message += "QT : {}".format(Qt.QT_VERSION_STR) + message += f"QT : {Qt.QT_VERSION_STR}" return message @@ -29,15 +30,15 @@ def _run_tests(test_suite, package_name, pattern): count = test_suite.countTestCases() print("######## Environment ########") print(pytest_report_header(None)) - print("{} tests has been discovered in {} with pattern {}".format(count, package_name, pattern)) + print(f"{count} tests has been discovered in {package_name} with pattern {pattern}") print("######## Running tests ########") results = unittest.TextTestRunner(verbosity=2).run(test_suite) print("######## Summary ########") - print("Errors : {}".format(len(results.errors))) - print("Failures : {}".format(len(results.failures))) - print("Expected failures : {}".format(len(results.expectedFailures))) - print("Unexpected successes : {}".format(len(results.unexpectedSuccesses))) - print("Skip : {}".format(len(results.skipped))) + print(f"Errors : {len(results.errors)}") + print(f"Failures : {len(results.failures)}") + print(f"Expected failures : {len(results.expectedFailures)}") + print(f"Unexpected successes : {len(results.unexpectedSuccesses)}") + print(f"Skip : {len(results.skipped)}") successes = results.testsRun - ( len(results.errors) + len(results.failures) @@ -45,8 +46,8 @@ def _run_tests(test_suite, package_name, pattern): + len(results.unexpectedSuccesses) + len(results.skipped) ) - print("Successes : {}".format(successes)) - print("TOTAL : {}".format(results.testsRun)) + print(f"Successes : {successes}") + print(f"TOTAL : {results.testsRun}") def test_package(package=None, pattern="test_*.py"): @@ -61,7 +62,7 @@ def test_package(package=None, pattern="test_*.py"): """ pattern_environment = os.environ.get("TEST_PATTERN") if pattern_environment and pattern_environment != "default_pattern": - print("Pattern from environment : {}".format(pattern_environment)) + print(f"Pattern from environment : {pattern_environment}") pattern = pattern_environment if package is None: diff --git a/tests/qgis/test_plg_dialogs.py b/tests/qgis/test_plg_dialogs.py index 7fe5586..74de559 100644 --- a/tests/qgis/test_plg_dialogs.py +++ b/tests/qgis/test_plg_dialogs.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - from qgis.testing import start_app, unittest from mesh_tools.libs.culvert_manager import CulvertManager diff --git a/tests/unit/test_pgl_metadata.py b/tests/unit/test_pgl_metadata.py index f85455f..81b311a 100644 --- a/tests/unit/test_pgl_metadata.py +++ b/tests/unit/test_pgl_metadata.py @@ -1,8 +1,5 @@ -# -*- coding: utf-8 -*- - # standard library import unittest - from pathlib import Path # 3rd party @@ -17,7 +14,6 @@ class TestPluginMetadata(unittest.TestCase): - """Test about module""" def test_metadata_types(self):