Skip to content

Commit cf2eb4c

Browse files
author
Axel Hörteborn
committed
Fixed the failing test
1 parent a2043ad commit cf2eb4c

3 files changed

Lines changed: 130 additions & 109 deletions

File tree

src/pytest_qgis/pytest_qgis.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import time
2525
import warnings
2626
from collections import namedtuple
27+
from collections.abc import Generator
2728
from pathlib import Path
2829
from typing import TYPE_CHECKING
2930
from unittest import mock
@@ -48,15 +49,11 @@
4849
from pytest_qgis.qgis_interface import QgisInterface
4950
from pytest_qgis.utils import (
5051
MessageLogCapture,
51-
MessageLogEntry,
5252
ensure_qgis_layer_fixtures_are_cleaned,
5353
get_common_extent_from_all_layers,
5454
get_layers_with_different_crs,
55-
make_memory_layer,
5655
replace_layers_with_reprojected_clones,
57-
run_task,
5856
set_map_crs_based_on_layers,
59-
wait_signal,
6057
)
6158

6259
if TYPE_CHECKING:
@@ -299,7 +296,9 @@ def qgis_bot(qgis_iface: QgisInterface) -> QgisBot:
299296

300297

301298
@pytest.fixture
302-
def qgis_message_log(qgis_app: QgsApplication): # noqa: ARG001
299+
def qgis_message_log(
300+
qgis_app: QgsApplication, # noqa: ARG001
301+
) -> Generator[MessageLogCapture, None, None]:
303302
"""
304303
Capture ``QgsMessageLog.logMessage`` emissions for the duration of
305304
a test.

src/pytest_qgis/utils.py

Lines changed: 80 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from collections.abc import Callable, Generator, Iterator, Mapping, Sequence
2323
from functools import wraps
2424
from pathlib import Path
25-
from typing import TYPE_CHECKING, Any, Union
25+
from typing import TYPE_CHECKING, Any
2626
from unittest.mock import MagicMock
2727

2828
from osgeo import gdal
@@ -31,8 +31,8 @@
3131
QgsCoordinateReferenceSystem,
3232
QgsCoordinateTransform,
3333
QgsFeature,
34-
QgsGeometry,
3534
QgsField,
35+
QgsGeometry,
3636
QgsLayerTree,
3737
QgsLayerTreeGroup,
3838
QgsLayerTreeLayer,
@@ -355,10 +355,8 @@ def run_task(
355355
if status in done_statuses or task.isCanceled():
356356
break
357357
# finalize hook so any cleanup logic runs on the main thread
358-
try:
358+
with contextlib.suppress(Exception):
359359
task.finished(result)
360-
except Exception:
361-
pass
362360
return bool(result)
363361

364362

@@ -400,16 +398,20 @@ def __enter__(self) -> "_SignalWaiter":
400398
self._signal.connect(self._on_signal)
401399
return self
402400

403-
def __exit__(self, exc_type, exc, tb) -> None:
401+
def __exit__(
402+
self,
403+
exc_type: type[BaseException] | None,
404+
exc: BaseException | None,
405+
tb: object,
406+
) -> None:
404407
try:
405408
if exc_type is None and not self.triggered:
406409
QTimer.singleShot(self._timeout_ms, self._on_timeout)
407410
self._loop.exec()
408411
finally:
409-
try:
412+
with contextlib.suppress(TypeError):
413+
# already disconnected
410414
self._signal.disconnect(self._on_signal)
411-
except TypeError:
412-
pass # already disconnected
413415

414416

415417
def wait_signal(
@@ -445,11 +447,8 @@ def wait_signal(
445447
# ---------------------------------------------------------------------------
446448

447449

448-
class MessageLogEntry(
449-
# Using tuple[str, str, int] via a lightweight namedtuple-ish class
450-
# keeps the API usable without an extra dependency on dataclasses.
451-
):
452-
__slots__ = ("message", "tag", "level")
450+
class MessageLogEntry:
451+
__slots__ = ("level", "message", "tag")
453452

454453
def __init__(self, message: str, tag: str, level: int) -> None:
455454
self.message = message
@@ -463,7 +462,9 @@ def __repr__(self) -> str: # pragma: no cover - debug aid
463462
Qgis.Critical: "Critical",
464463
Qgis.Success: "Success",
465464
}.get(self.level, str(self.level))
466-
return f"MessageLogEntry({lvl_name}, tag={self.tag!r}, message={self.message!r})"
465+
return (
466+
f"MessageLogEntry({lvl_name}, tag={self.tag!r}, message={self.message!r})"
467+
)
467468

468469
def __iter__(self) -> Iterator[Any]:
469470
# Allow tuple-style unpacking: message, tag, level = entry
@@ -497,22 +498,18 @@ def _wrap_log_message(
497498
self.entries.append(MessageLogEntry(message, tag, level))
498499
# Delegate to the original so the real logger still does its thing.
499500
if self._original is not None:
500-
try:
501+
with contextlib.suppress(
502+
Exception
503+
): # pragma: no cover - never mask test state
501504
self._original(message, tag, level)
502-
except Exception: # pragma: no cover - never mask test state
503-
pass
504505

505506
def connect(self) -> None:
506-
from qgis.core import QgsMessageLog # noqa: PLC0415 - avoid top-level hit
507-
508507
if self._original is not None:
509508
return # already connected
510509
self._original = QgsMessageLog.logMessage
511510
QgsMessageLog.logMessage = staticmethod(self._wrap_log_message)
512511

513512
def disconnect(self) -> None:
514-
from qgis.core import QgsMessageLog # noqa: PLC0415
515-
516513
if self._original is None:
517514
return
518515
QgsMessageLog.logMessage = staticmethod(self._original)
@@ -558,7 +555,7 @@ def clear(self) -> None:
558555
self.entries.clear()
559556

560557

561-
def _get_qgs_application():
558+
def _get_qgs_application() -> type:
562559
"""Internal helper -- avoids a top-level import cycle."""
563560
from qgis.core import QgsApplication as _QgsApplication # noqa: PLC0415
564561

@@ -589,16 +586,61 @@ def _infer_geometry_kind(sample: str) -> str:
589586
return _GEOMETRY_KIND_BY_WKT_PREFIX[prefix]
590587

591588

589+
# Field types are kept as QVariant constants so pytest-qgis stays compatible
590+
# with both QGIS 3 (Qt5/QVariant) and QGIS 4 (Qt6/QMetaType — QVariant.* still
591+
# resolves to the same numeric values). See flake8-qgis QGS402.
592592
_PYTHON_TYPE_TO_QVARIANT: Mapping[type, Any] = {
593-
int: QVariant.Int,
594-
float: QVariant.Double,
595-
str: QVariant.String,
596-
bool: QVariant.Bool,
593+
int: QVariant.Int, # noqa: QGS402
594+
float: QVariant.Double, # noqa: QGS402
595+
str: QVariant.String, # noqa: QGS402
596+
bool: QVariant.Bool, # noqa: QGS402
597597
}
598598

599599

600+
def _normalise_features(
601+
features: Sequence[str | tuple[str, Mapping[str, Any]]],
602+
) -> list[tuple[str, dict[str, Any]]]:
603+
normalised: list[tuple[str, dict[str, Any]]] = []
604+
for item in features:
605+
if isinstance(item, str):
606+
normalised.append((item, {}))
607+
else:
608+
wkt, attrs = item
609+
normalised.append((wkt, dict(attrs)))
610+
return normalised
611+
612+
613+
def _build_qgs_fields(fields: Mapping[str, type]) -> list[QgsField]:
614+
qgs_fields: list[QgsField] = []
615+
for fname, fpy_type in fields.items():
616+
qvariant = _PYTHON_TYPE_TO_QVARIANT.get(fpy_type)
617+
if qvariant is None:
618+
raise TypeError(
619+
f"Field {fname!r}: unsupported type {fpy_type!r}. "
620+
f"Supported: {sorted(t.__name__ for t in _PYTHON_TYPE_TO_QVARIANT)}"
621+
)
622+
qgs_fields.append(QgsField(fname, qvariant))
623+
return qgs_fields
624+
625+
626+
def _make_feature(
627+
layer: QgsVectorLayer,
628+
wkt: str,
629+
attrs: Mapping[str, Any],
630+
field_names: Sequence[str],
631+
) -> QgsFeature:
632+
geom = QgsGeometry.fromWkt(wkt)
633+
if geom.isEmpty():
634+
raise ValueError(f"Could not parse WKT: {wkt!r}")
635+
feat = QgsFeature(layer.fields())
636+
feat.setGeometry(geom)
637+
for fname in field_names:
638+
feat.setAttribute(fname, attrs.get(fname))
639+
return feat
640+
641+
600642
def make_memory_layer(
601-
features: Sequence[Union[str, tuple[str, Mapping[str, Any]]]],
643+
features: Sequence[str | tuple[str, Mapping[str, Any]]],
602644
*,
603645
fields: Mapping[str, type] | None = None,
604646
crs: str = "EPSG:4326",
@@ -636,57 +678,29 @@ def make_memory_layer(
636678
if not features:
637679
raise ValueError("features must be a non-empty sequence")
638680

639-
# Normalise input to (wkt, attrs) pairs and infer schema.
640-
normalised: list[tuple[str, dict[str, Any]]] = []
641-
for item in features:
642-
if isinstance(item, str):
643-
normalised.append((item, {}))
644-
else:
645-
wkt, attrs = item
646-
normalised.append((wkt, dict(attrs)))
647-
681+
normalised = _normalise_features(features)
648682
if geometry_kind is None:
649683
geometry_kind = _infer_geometry_kind(normalised[0][0])
650-
651684
if fields is None:
652-
first_attrs = normalised[0][1]
653-
fields = {k: type(v) for k, v in first_attrs.items()}
685+
fields = {k: type(v) for k, v in normalised[0][1].items()}
654686

655-
# Create the layer.
656-
layer = QgsVectorLayer(
657-
f"{geometry_kind}?crs={crs}", name, "memory"
658-
)
687+
layer = QgsVectorLayer(f"{geometry_kind}?crs={crs}", name, "memory")
659688
if not layer.isValid():
660689
raise RuntimeError(
661690
f"Failed to construct memory layer (geom={geometry_kind}, crs={crs})"
662691
)
663692
pr = layer.dataProvider()
664693

665-
# Schema.
666-
qgs_fields: list[QgsField] = []
667-
for fname, fpy_type in fields.items():
668-
qvariant = _PYTHON_TYPE_TO_QVARIANT.get(fpy_type)
669-
if qvariant is None:
670-
raise TypeError(
671-
f"Field {fname!r}: unsupported type {fpy_type!r}. "
672-
f"Supported: {sorted(t.__name__ for t in _PYTHON_TYPE_TO_QVARIANT)}"
673-
)
674-
qgs_fields.append(QgsField(fname, qvariant))
675-
if qgs_fields:
676-
pr.addAttributes(qgs_fields)
677-
layer.updateFields()
694+
qgs_fields = _build_qgs_fields(fields)
695+
if qgs_fields and not pr.addAttributes(qgs_fields):
696+
raise RuntimeError(f"Failed to add attributes to memory layer: {qgs_fields!r}")
697+
layer.updateFields()
678698

679-
# Features.
680699
field_names = list(fields.keys())
681700
for wkt, attrs in normalised:
682-
geom = QgsGeometry.fromWkt(wkt)
683-
if geom.isEmpty():
684-
raise ValueError(f"Could not parse WKT: {wkt!r}")
685-
feat = QgsFeature(layer.fields())
686-
feat.setGeometry(geom)
687-
for fname in field_names:
688-
feat.setAttribute(fname, attrs.get(fname))
689-
pr.addFeature(feat)
701+
feat = _make_feature(layer, wkt, attrs, field_names)
702+
if not pr.addFeature(feat):
703+
raise RuntimeError(f"Failed to add feature to memory layer: {wkt!r}")
690704

691705
layer.updateExtents()
692706
return layer

0 commit comments

Comments
 (0)