Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added clean_qgis_layer decorator back #64

Merged
merged 1 commit into from
Jun 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 32 additions & 1 deletion src/pytest_qgis/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
17 changes: 16 additions & 1 deletion tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Loading