From 384d94e00874e39dc5ddeede355866d7535898b0 Mon Sep 17 00:00:00 2001 From: Joonalai Date: Mon, 13 May 2024 23:34:48 +0300 Subject: [PATCH] Add the clean_qgis_layer decorator back --- CHANGELOG.md | 5 +++++ README.md | 29 +++++++++++++++++++++++++++++ src/pytest_qgis/utils.py | 33 ++++++++++++++++++++++++++++++++- tests/test_utils.py | 17 ++++++++++++++++- 4 files changed, 82 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a6d648..a932016 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Unreleased +## New Features + +* Add `clean_qgis_layer` decorator back alongside with automatic cleaning [#45](https://github.com/GispoCoding/pytest-qgis/pull/45) + + ## Fixes * [#55](https://github.com/GispoCoding/pytest-qgis/pull/55) Allows using MagicMocks to mock layers without problems diff --git a/README.md b/README.md index 7d57e45..11dbee3 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,35 @@ markers can be used. * `pytest_runtest_teardown` hook is used to ensure that all layer fixtures of any scope are cleaned properly without causing segmentation faults. The layer fixtures that are cleaned automatically must have some of the following keywords in their name: "layer", "lyr", "raster", "rast", "tif". + +### Utility tools + +* `clean_qgis_layer` decorator found in `pytest_qgis.utils` can be used with `QgsMapLayer` fixtures to ensure that they + are cleaned properly if they are used but not added to the `QgsProject`. This is only needed with layers with other than memory provider. + + This decorator works only with fixtures that **return** QgsMapLayer instances. + There is no support for fixtures that use yield. + + This decorator is an alternative way of cleaning the layers, since `pytest_runtest_teardown` hook cleans layer fixtures automatically by the keyword. + + ```python + # conftest.py or start of a test file + import pytest + from pytest_qgis.utils import clean_qgis_layer + from qgis.core import QgsVectorLayer + + @pytest.fixture() + @clean_qgis_layer + def geojson() -> QgsVectorLayer: + return QgsVectorLayer("layer_file.geojson", "some layer") + + # This will be cleaned automatically since it contains the keyword "layer" in its name + @pytest.fixture() + def geojson_layer() -> QgsVectorLayer: + return QgsVectorLayer("layer_file2.geojson", "some layer") + ``` + + ### Command line options * `--qgis_disable_gui` can be used to disable graphical user interface in tests. This speeds up the tests that use Qt diff --git a/src/pytest_qgis/utils.py b/src/pytest_qgis/utils.py index 5330ee5..c650055 100644 --- a/src/pytest_qgis/utils.py +++ b/src/pytest_qgis/utils.py @@ -18,8 +18,9 @@ # import time from collections import Counter +from functools import wraps from pathlib import Path -from typing import TYPE_CHECKING, Any, Optional +from typing import TYPE_CHECKING, Any, Callable, Generator, Optional from unittest.mock import MagicMock from osgeo import gdal @@ -182,6 +183,36 @@ def copy_layer_style_and_position( group.insertLayer(index + 1, layer2) +def clean_qgis_layer(fn: Callable[..., QgsMapLayer]) -> Callable[..., QgsMapLayer]: + """ + Decorator to ensure that a map layer created by a fixture is cleaned properly. + + Sometimes fixture non-memory layers that are used but not added + to the project might cause segmentation fault errors. + + This decorator works only with fixtures that **return** QgsMapLayer instances. + There is no support for fixtures that use yield. + + >>> @pytest.fixture() + >>> @clean_qgis_layer + >>> def geojson_layer() -> QgsVectorLayer: + >>> layer = QgsVectorLayer("layer.json", "layer", "ogr") + >>> return layer + + This decorator is the alternative way of cleaning the layers since layer fixtures + are automatically cleaned if they contain one of the keywords listed in + LAYER_KEYWORDS by pytest_runtest_teardown hook. + """ + + @wraps(fn) + def wrapper(*args: Any, **kwargs: Any) -> Generator[QgsMapLayer, None, None]: + layer = fn(*args, **kwargs) + yield layer + _set_layer_owner_to_project(layer) + + return wrapper + + def ensure_qgis_layer_fixtures_are_cleaned(request: "FixtureRequest") -> None: """ Sometimes fixture non-memory layers that are used but not added diff --git a/tests/test_utils.py b/tests/test_utils.py index fda0d59..b034892 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -18,12 +18,14 @@ import pytest from pytest_qgis.utils import ( + clean_qgis_layer, get_common_extent_from_all_layers, get_layers_with_different_crs, replace_layers_with_reprojected_clones, set_map_crs_based_on_layers, ) -from qgis.core import QgsCoordinateReferenceSystem, QgsProject +from qgis.core import QgsCoordinateReferenceSystem, QgsProject, QgsVectorLayer +from qgis.PyQt import sip from tests.utils import EPSG_3067, EPSG_4326, QGIS_VERSION @@ -98,3 +100,16 @@ def test_replace_layers_with_reprojected_clones( # noqa: PLR0913 assert layers[raster_layer_name].crs().authid() == EPSG_4326 assert (tmp_path / f"{vector_layer_id}.qml").exists() assert (tmp_path / f"{raster_layer_id}.qml").exists() + + +def test_clean_qgis_layer(layer_polygon): + layer = QgsVectorLayer(layer_polygon.source(), "another layer") + + @clean_qgis_layer + def layer_function() -> QgsVectorLayer: + return layer + + # Using list to trigger yield and the code that runs after it + list(layer_function()) + + assert sip.isdeleted(layer)