diff --git a/.docker/docker-compose.yml b/.docker/docker-compose.yml
index 623479cb..f8ae164a 100644
--- a/.docker/docker-compose.yml
+++ b/.docker/docker-compose.yml
@@ -1,7 +1,7 @@
services:
qgis:
- user: ${UID}:${GID}
image: ${QGIS_IMAGE_TAG}
+ user: ${UID}:${GID}
network_mode: host
container_name: qgis-lizmap-tests
environment:
diff --git a/.docker/run-tests.sh b/.docker/run-tests.sh
index 067fcfb3..b8af5ed8 100644
--- a/.docker/run-tests.sh
+++ b/.docker/run-tests.sh
@@ -13,6 +13,8 @@ python3 -m venv $VENV --system-site-package
echo "Installing requirements..."
$VENV/bin/pip install -q --no-cache -r requirements/tests.txt
+export PYTHONPATH="/usr/share/qgis/python/:$PYTHONPATH"
+
cd tests && $VENV/bin/python -m pytest -v
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index bd3be363..5a8c59aa 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -16,13 +16,13 @@ jobs:
python-version: "3.12"
architecture: x64
cache: "pip"
- cache-dependency-path: "requirements/dev.txt"
+ cache-dependency-path: "requirements/lint.txt"
- name: Install Python requirements
run: pip install --quiet -r requirements/lint.txt
- name: Run linter
- run: ruff check --preview --output-format=concise lizmap
+ run: ruff check --output-format=concise lizmap
- name: Run security check
run: bandit -r lizmap -ll
@@ -39,6 +39,7 @@ jobs:
"3.34",
"3.40",
"3.44",
+ "4.0",
]
steps:
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 7b048284..177fc49d 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -3,16 +3,58 @@ variables:
stages:
- lint
+- test
- build
- deploy
+# ------------
+# Lint
+# ------------
+
lint:
- image: $REGISTRY_URI/factory-ci-runner:qgis-ltr
- stage: lint
+ image: ${REGISTRY_URL}/factory-ci-runner:qgis-${QGIS_FLAVOR}
tags:
- factory-plain
+ stage: lint
script:
+ # No need since we do not type checking atm
+ #- pip install --quiet -r requirements/dev.txt
- make lint
+ parallel:
+ matrix:
+ - QGIS_FLAVOR: [ "ltr", "release" ]
+
+# ------------
+# Tests
+# ------------
+
+.tests:
+ image: ${REGISTRY_URL}/factory-ci-runner:factory-ci
+ tags:
+ - factory-dind
+ stage: test
+ script:
+ - make docker-test QGIS_VERSION=$QGIS_FLAVOR
+
+qgis-tests:
+ extends: .tests
+ parallel:
+ matrix:
+ - QGIS_FLAVOR: [
+ "3.34",
+ "3.40",
+ "3.44",
+ "4.0",
+ ]
+
+
+# NOTE: Following jobs are triggered only on RELEASE_BRANCH == "true"
+
+#
+# Packages
+#
+
+>>>>>>> 6c46c59 (Fix tests)
build:
image: $REGISTRY_URI/factory-ci-runner:qgis-plugin-ci
diff --git a/Makefile b/Makefile
index 580fffcb..5b5f59b7 100644
--- a/Makefile
+++ b/Makefile
@@ -2,7 +2,6 @@ SHELL:=bash
PYTHON_MODULE=lizmap
-QGIS_VERSION ?= 3.40
-include .localconfig.mk
@@ -17,7 +16,7 @@ ifdef VIRTUAL_ENV
# Always prefer active environment
ACTIVE_VENV=--active
endif
-UV_RUN=uv run $(ACTIVE_VENV)
+UV=uv run $(ACTIVE_VENV)
endif
@@ -32,13 +31,6 @@ REQUIREMENTS_GROUPS= \
REQUIREMENTS=$(patsubst %, requirements/%.txt, $(REQUIREMENTS_GROUPS))
-# Update only packaging dependencies
-# Waiting for https://github.com/astral-sh/uv/issues/13705
-update-packaging-dependencies::
- uv lock -P qgis-plugin-package-ci -P qgis-plugin-transifex-ci
-
-update-packaging-dependencies:: update-requirements
-
update-requirements: $(REQUIREMENTS)
# Require uv (https://docs.astral.sh/uv/) for extracting
@@ -56,53 +48,92 @@ requirements/%.txt: uv.lock
# Static analysis
#
-LINT_TARGETS=$(PYTHON_MODULE) tests $(EXTRA_LINT_TARGETS)
+LINT_TARGETS=$(PYTHON_MODULE) $(EXTRA_LINT_TARGETS)
lint:
- @ $(UV_RUN) ruff check --output-format=concise $(LINT_TARGETS)
+ @ $(UV) ruff check --output-format=concise $(LINT_TARGETS)
+ @ $(UV) ruff check --output-format=concise --target-version=py310 tests
lint-fix:
- @ $(UV_RUN) ruff check --fix $(LINT_TARGETS)
+ @ $(UV) ruff check --fix $(LINT_TARGETS)
+ @ $(UV) ruff check --fix --target-version=py310 tests
+
+lint-preview:
+ @ $(UV) ruff check --preview --output-format=concise $(LINT_TARGETS)
format:
- @ $(UV_RUN) ruff format $(LINT_TARGETS)
+ @ $(UV) ruff format $(LINT_TARGETS)
typecheck:
- $(UV_RUN) mypy $(PYTHON_MODULE)
+ $(UV) mypy $(PYTHON_MODULE)
+ $(UV) mypy tests --python-version 3.10
scan:
- @ $(UV_RUN) bandit -r $(PYTHON_MODULE) $(SCAN_OPTS)
+ @ $(UV) bandit -r $(PYTHON_MODULE) $(SCAN_OPTS)
-check-uv-install:
- @which uv > /dev/null || { \
- echo "You must install uv (https://docs.astral.sh/uv/)"; \
- exit 1; \
- }
-
#
# Tests
#
test:
- $(UV_RUN) pytest -v tests/
+ $(UV) pytest -v tests/
#
# Test using docker image
#
-QGIS_IMAGE_REPOSITORY ?= qgis/qgis
+
+ifdef REGISTRY_URL
+REGISTRY_PREFIX=$(REGISTRY_URL)/
+else
+REGISTRY_PREFIX=3liz/
+endif
+
+QGIS_VERSION ?= 3.44
+QGIS_IMAGE_REPOSITORY ?= ${REGISTRY_PREFIX}qgis-platform
QGIS_IMAGE_TAG ?= $(QGIS_IMAGE_REPOSITORY):$(QGIS_VERSION)
export QGIS_VERSION
export QGIS_IMAGE_TAG
export UID=$(shell id -u)
export GID=$(shell id -g)
+
docker-test:
- cd .docker && docker compose up \
+ set -e; \
+ cd .docker; \
+ docker compose up \
--quiet-pull \
--abort-on-container-exit \
- --exit-code-from qgis
- cd .docker && docker compose down -v
+ --exit-code-from qgis; \
+ docker compose down -v;
+
+#
+# Install/sync
+#
+
+sync:
+ @echo "Synchronizing python's environment with frozen dependencies"
+ uv sync --all-groups --frozen --all-extras
+
+install-dev::
+ @echo "Creating virtual python environment"
+ uv venv --system-site-packages --no-managed-python
+
+install-dev:: sync
+
+#
+# Coverage
+#
+
+# Run tests coverage
+covtests:
+ @echo "Running coverage tests"
+ @ $(UV) coverage run -m pytest tests/
+
+coverage: covtests
+ @echo "Building coverage html"
+ @ $(UV) coverage html
+
#
# Code managment
diff --git a/lizmap/dialogs/dock_html_preview.py b/lizmap/dialogs/dock_html_preview.py
index 62a93ab1..2492ed41 100644
--- a/lizmap/dialogs/dock_html_preview.py
+++ b/lizmap/dialogs/dock_html_preview.py
@@ -1,9 +1,8 @@
+"""
__copyright__ = 'Copyright 2023, 3Liz'
__license__ = 'GPL version 3'
__email__ = 'info@3liz.org'
-
-import logging
-
+"""
from qgis.core import (
QgsApplication,
QgsExpression,
@@ -26,10 +25,8 @@
)
from qgis.utils import iface
-from lizmap.toolbelt.i18n import tr
-from lizmap.toolbelt.resources import resources_path
-
-LOGGER = logging.getLogger('Lizmap')
+from ..toolbelt.i18n import tr
+from ..toolbelt.resources import resources_path
# Detect available Web widget
WEBKIT_AVAILABLE = False
diff --git a/lizmap/dialogs/html_editor.py b/lizmap/dialogs/html_editor.py
index f6da2a42..98238d93 100644
--- a/lizmap/dialogs/html_editor.py
+++ b/lizmap/dialogs/html_editor.py
@@ -1,16 +1,13 @@
+"""
__copyright__ = 'Copyright 2023, 3Liz'
__license__ = 'GPL version 3'
__email__ = 'info@3liz.org'
-
-import logging
-
+"""
from qgis.core import QgsVectorLayer
from qgis.PyQt.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout
-from lizmap.toolbelt.i18n import tr
-from lizmap.widgets.html_editor import HtmlEditorWidget
-
-LOGGER = logging.getLogger('Lizmap')
+from ..toolbelt.i18n import tr
+from ..widgets.html_editor import HtmlEditorWidget
class HtmlEditorDialog(QDialog):
diff --git a/lizmap/dialogs/main.py b/lizmap/dialogs/main.py
index 0c62564c..e9ec2af0 100644
--- a/lizmap/dialogs/main.py
+++ b/lizmap/dialogs/main.py
@@ -1,9 +1,5 @@
-from __future__ import annotations
-
-import logging
-
from pathlib import Path
-from typing import TYPE_CHECKING, Any
+from typing import Any
from qgis.core import (
Qgis,
@@ -29,6 +25,7 @@
QPushButton,
QSizePolicy,
QSpacerItem,
+ QWidget,
)
from qgis.utils import OverrideCursor, iface
@@ -64,15 +61,17 @@
simplify_provider_side,
use_estimated_metadata,
)
-from lizmap.qt_style_sheets import COMPLETE_STYLE_SHEET
-from lizmap.saas import fix_ssl, is_lizmap_cloud
-from lizmap.table_manager.upload_files import TableFilesManager
-from lizmap.toolbelt.i18n import tr
-from lizmap.toolbelt.layer import relative_path
-from lizmap.toolbelt.resources import load_ui, resources_path
-from lizmap.toolbelt.strings import human_size
-from lizmap.toolbelt.version import qgis_version_info
-from lizmap.widgets.check_project import Checks, Headers, TableCheck
+
+from .. import logger
+from ..qt_style_sheets import COMPLETE_STYLE_SHEET
+from ..saas import fix_ssl, is_lizmap_cloud
+from ..table_manager.upload_files import TableFilesManager
+from ..toolbelt.i18n import tr
+from ..toolbelt.layer import relative_path
+from ..toolbelt.resources import load_ui, resources_path
+from ..toolbelt.strings import human_size
+from ..toolbelt.version import qgis_version_info
+from ..widgets.check_project import Checks, Headers, TableCheck
WEBKIT_AVAILABLE = False
try:
@@ -94,12 +93,8 @@
WEB_ENGINE = False
WEBKIT_AVAILABLE = False
-if TYPE_CHECKING:
- from qgis.PyQt.QtWidgets import QWidget
-
FORM_CLASS = load_ui('ui_lizmap.ui')
-LOGGER = logging.getLogger("Lizmap")
class LizmapDialog(QDialog, FORM_CLASS):
@@ -734,7 +729,7 @@ def check_qgis_version(self, message_bar: bool = False, widget: bool = False) ->
return False
title = tr('QGIS server version is lower than QGIS desktop version')
- LOGGER.error(title)
+ logger.error(title)
description = tr('Your QGIS desktop is writing QGS project in the future compare to QGIS server.')
@@ -1373,7 +1368,7 @@ def select_unknown_features_group(self):
")"
)
layer.removeSelection()
- LOGGER.debug("Expression used for checking groups not on the server :\n" + expression)
+ logger.debug("Expression used for checking groups not on the server :\n" + expression)
layer.selectByExpression(expression)
count = layer.selectedFeatureCount()
self.display_message_bar(
@@ -1429,5 +1424,5 @@ def activateWindow(self):
""" When the dialog displayed, to trigger functions in the plugin when the dialog is opening. """
self.check_project_thumbnail()
self.check_action_file_exists()
- LOGGER.info("Opening the Lizmap dialog.")
+ logger.info("Opening the Lizmap dialog.")
super().activateWindow()
diff --git a/lizmap/dialogs/news.py b/lizmap/dialogs/news.py
index 5379cd6c..81862509 100644
--- a/lizmap/dialogs/news.py
+++ b/lizmap/dialogs/news.py
@@ -1,21 +1,19 @@
+"""
__copyright__ = 'Copyright 2024, 3Liz'
__license__ = 'GPL version 3'
__email__ = 'info@3liz.org'
-
-import logging
-
+"""
from qgis.core import QgsSettings
from qgis.PyQt.QtCore import Qt, QUrl
from qgis.PyQt.QtGui import QDesktopServices, QPixmap
from qgis.PyQt.QtWidgets import QDialog, QDialogButtonBox
-from lizmap.definitions.definitions import LwcVersions
-from lizmap.definitions.online_help import online_lwc_help
-from lizmap.definitions.qgis_settings import Settings
-from lizmap.toolbelt.i18n import tr
-from lizmap.toolbelt.resources import load_ui, resources_path
+from ..definitions.definitions import LwcVersions
+from ..definitions.online_help import online_lwc_help
+from ..definitions.qgis_settings import Settings
+from ..toolbelt.i18n import tr
+from ..toolbelt.resources import load_ui, resources_path
-LOGGER = logging.getLogger('Lizmap')
FORM_CLASS = load_ui('ui_news.ui')
diff --git a/lizmap/dialogs/server_wizard.py b/lizmap/dialogs/server_wizard.py
index db1ff35c..f2b45bf9 100644
--- a/lizmap/dialogs/server_wizard.py
+++ b/lizmap/dialogs/server_wizard.py
@@ -1,16 +1,13 @@
-from __future__ import annotations
-
import configparser
import json
-import logging
import sys
from base64 import b64encode
from enum import IntEnum, auto
from functools import partial
-from typing import TYPE_CHECKING
from qgis.core import (
+ Qgis,
QgsAbstractDatabaseProviderConnection,
QgsApplication,
QgsAuthMethodConfig,
@@ -41,26 +38,22 @@
QSpacerItem,
QSpinBox,
QVBoxLayout,
+ QWidget,
QWizard,
QWizardPage,
)
from qgis.utils import OverrideCursor, iface
-from lizmap.definitions.definitions import UNSTABLE_VERSION_PREFIX
-from lizmap.definitions.online_help import online_lwc_help
-from lizmap.definitions.qgis_settings import Settings
-from lizmap.logger import log_function
-from lizmap.saas import is_lizmap_cloud, webdav_properties
-from lizmap.server_dav import WebDav
-from lizmap.toolbelt.i18n import tr
-from lizmap.toolbelt.plugin import lizmap_user_folder, user_settings
-from lizmap.toolbelt.version import version
-
-if TYPE_CHECKING:
- from qgis.PyQt.QtWidgets import QWidget
-
+from .. import logger
+from ..definitions.definitions import UNSTABLE_VERSION_PREFIX
+from ..definitions.online_help import online_lwc_help
+from ..definitions.qgis_settings import Settings
+from ..saas import is_lizmap_cloud, webdav_properties
+from ..server_dav import WebDav
+from ..toolbelt.i18n import tr
+from ..toolbelt.plugin import lizmap_user_folder, user_settings
+from ..toolbelt.version import version
-LOGGER = logging.getLogger('Lizmap')
THUMBS = " 👍"
DEBUG = True
@@ -340,10 +333,10 @@ def __init__(self, parent: QWidget | None = None):
layout.addWidget(label_warning)
if QgsApplication.authManager().masterPasswordIsSet():
- LOGGER.debug("Master password is set : False")
+ logger.debug("Master password is set : False")
label_warning.setVisible(False)
else:
- LOGGER.debug("Master password is set : True")
+ logger.debug("Master password is set : True")
label_warning.setVisible(True)
self.result_master_password = QLabel()
@@ -351,17 +344,17 @@ def __init__(self, parent: QWidget | None = None):
# noinspection PyArgumentList
layout.addWidget(self.result_master_password)
- @log_function
+ @logger.log_function
def nextId(self) -> int:
""" Next page, according to lizmap.com hosting. """
# Temporary disable the PG page
# parent_wizard: ServerWizard = self.wizard()
# if parent_wizard.is_lizmap_cloud:
- # LOGGER.debug("After saving the auth ID, go the PostgreSQL page.")
+ # logger.debug("After saving the auth ID, go the PostgreSQL page.")
# return WizardPages.AddOrNotPostgresqlPage
# Finished
- LOGGER.debug("After saving the auth ID, it's finished")
+ logger.debug("After saving the auth ID, it's finished")
return -1
@@ -371,7 +364,7 @@ class AddOrNotPostgresqlPage(QWizardPage):
def __init__(self, parent: QWidget | None = None):
super().__init__(parent)
- LOGGER.debug("Page : Add the PostgreSQL connection delivered with Lizmap")
+ logger.debug("Page : Add the PostgreSQL connection delivered with Lizmap")
self.setTitle(tr("PostgreSQL"))
layout = QVBoxLayout()
@@ -401,12 +394,12 @@ def __init__(self, parent: QWidget | None = None):
# def isComplete(self) -> bool:
# """ Form validation before the next step. """
- # LOGGER.debug("Calling AddOrNotPGPage::isComplete")
+ # logger.debug("Calling AddOrNotPGPage::isComplete")
# if self.field("postgresql_yes"):
# # Add PG
# # self.wizard().button(QWizard.NextButton).setVisible(True)
# self.setFinalPage(False)
- # LOGGER.debug("Enf of function isComplete, returning True to PG page")
+ # logger.debug("Enf of function isComplete, returning True to PG page")
# self.completeChanged.emit()
# return True
#
@@ -416,7 +409,7 @@ def __init__(self, parent: QWidget | None = None):
# # No webdav module
# self.setFinalPage(True)
# # self.wizard().button(QWizard.NextButton).setVisible(False)
- # LOGGER.debug("Enf of function isComplete, returning True, no dav")
+ # logger.debug("Enf of function isComplete, returning True, no dav")
# self.completeChanged.emit()
# return True
#
@@ -424,21 +417,21 @@ def __init__(self, parent: QWidget | None = None):
# # Already has some repository, we do not suggest a new one
# self.setFinalPage(True)
# # self.wizard().button(QWizard.NextButton).setVisible(False)
- # LOGGER.debug("Enf of function isComplete, returning True, already repositories")
+ # logger.debug("Enf of function isComplete, returning True, already repositories")
# self.completeChanged.emit()
# return True
#
# # Webdav repository
# self.setFinalPage(False)
# # self.wizard().button(QWizard.NextButton).setVisible(True)
- # LOGGER.debug("Enf of function isComplete, returning True to add webdav directory")
+ # logger.debug("Enf of function isComplete, returning True to add webdav directory")
# self.completeChanged.emit()
# return True
- @log_function
+ @logger.log_function
def nextId(self) -> int:
""" Next step. """
- LOGGER.debug("Calling AddOrNotPGPage::nextId")
+ logger.debug("Calling AddOrNotPGPage::nextId")
if self.field("postgresql_yes"):
return WizardPages.PostgresqlPage
@@ -694,7 +687,7 @@ def create_remote_directory(self):
dav_url = parent_wizard.dav_url
auth_id = parent_wizard.auth_id
- LOGGER.debug(f"Creating a folder called '{self.custom_name.text()}' on {dav_url}")
+ logger.debug(f"Creating a folder called '{self.custom_name.text()}' on {dav_url}")
with OverrideCursor(Qt.CursorShape.WaitCursor):
server_dav = WebDav(dav_url, auth_id)
if parent_wizard._user:
@@ -878,7 +871,12 @@ def __init__(
self.setPage(WizardPages.CreateNewFolderDav, CreateNewFolderDavPage())
self.setPage(WizardPages.LizmapNewRepository, LizmapNewRepositoryPage())
- @log_function
+ # TODO: If these methods raises an exception while called
+ # by the Qt framework this cause a "Fatal Python error"
+ # These method should be bounded by try/except when used from
+ # Qt framework.
+
+ @logger.log_function
def validateCurrentPage(self):
"""Specific rules for page validation. """
if self.currentId() == WizardPages.LoginPasswordPage:
@@ -919,9 +917,9 @@ def validateCurrentPage(self):
return False
if self.currentId() == WizardPages.MasterPasswordPage:
- LOGGER.debug("Validate current page, going to save the auth")
+ logger.debug("Validate current page, going to save the auth")
result = self.save_auth_id()
- LOGGER.debug("Saving to the authentication database is : {} valid".format("" if result else "not"))
+ logger.debug("Saving to the authentication database is : {} valid".format("" if result else "not"))
return result
if self.currentId() == WizardPages.PostgresqlPage:
@@ -951,14 +949,14 @@ def current_login(self) -> str:
""" Cleaned input login. """
return self.clean_data(self.field("login"))
- @log_function
+ @logger.log_function
def save_auth_id(self) -> bool:
""" Save login and password in the QGIS password manager.
Only if it's a new server, it will be saved in the JSON file.
"""
if self.page(WizardPages.LoginPasswordPage).auth_widget.configId() != "":
- LOGGER.info("The user is advanced, he used an existing auth, skip saving.")
+ logger.info("The user is advanced, he used an existing auth, skip saving.")
self.auth_id = self.page(WizardPages.LoginPasswordPage).auth_widget.configId()
self.save_json_server()
return True
@@ -978,7 +976,7 @@ def save_auth_id(self) -> bool:
# noinspection PyArgumentList,PyUnresolvedReferences
config.setConfig('realm', QUrl(url).host())
if self.auth_id:
- LOGGER.debug(f"Edit current information authentication ID : {self.auth_id}")
+ logger.debug(f"Edit current information authentication ID : {self.auth_id}")
# Edit
config.setId(self.auth_id)
result = auth_manager.storeAuthenticationConfig(config, True)
@@ -986,27 +984,27 @@ def save_auth_id(self) -> bool:
else:
# Creation
self.auth_id = auth_manager.uniqueConfigId()
- LOGGER.debug(f"New authentication ID : {self.auth_id} is going to be created")
+ logger.debug(f"New authentication ID : {self.auth_id} is going to be created")
config.setId(self.auth_id)
result = auth_manager.storeAuthenticationConfig(config)
- LOGGER.debug(f"New auth ID {self.auth_id} created")
+ logger.debug(f"New auth ID {self.auth_id} created")
if result[0]:
# Only for creation of the server, we save in the JSON
self.save_json_server()
if result[0]:
- LOGGER.debug("Set thumbs")
+ logger.debug("Set thumbs")
self.currentPage().result_master_password.setText(THUMBS)
- LOGGER.info(
+ logger.info(
f"Saving configuration with login/password ID {self.auth_id} = OK")
return True
- LOGGER.warning(
+ logger.warning(
f"Saving configuration with login/password ID {self.auth_id} = NOK")
self.currentPage().result_master_password.setText(
tr("We couldn't save the login/password into the QGIS authentication database : NOK")
)
- LOGGER.debug("Leaving function save_auth_id")
+ logger.debug("Leaving function save_auth_id")
return False
def save_json_server(self):
@@ -1033,7 +1031,7 @@ def save_json_server(self):
with open(user_settings(), 'w') as json_file:
json_file.write(file_content)
- LOGGER.debug("Server saved in the JSON file")
+ logger.debug("Server saved in the JSON file")
@classmethod
def override_url(cls, base_url: str, metadata: bool = True) -> str | None:
@@ -1049,7 +1047,7 @@ def override_url(cls, base_url: str, metadata: bool = True) -> str | None:
if base_url not in config.sections() and base_url[0:-1] not in config.sections():
return None
- LOGGER.info(f"Found a server override for server {base_url}")
+ logger.info(f"Found a server override for server {base_url}")
key = 'metadata' if metadata else 'dataviz'
try:
@@ -1094,7 +1092,12 @@ def request_check_url(self, url: str, login: str, password: str) -> tuple[bool,
net_req.setUrl(QUrl(url))
token = b64encode(f"{login}:{password}".encode())
net_req.setRawHeader(b"Authorization", b"Basic %s" % token)
- net_req.setAttribute(QNetworkRequest.FollowRedirectsAttribute, True)
+ # NOTE: According to QT6 doc this is enabled by default
+ # See https://doc.qt.io/archives/qt-5.15/qnetworkrequest.html#RedirectPolicy-enum
+ # See https://doc.qt.io/qt-6/network-changes-qt6.html#redirect-policies
+ if Qgis.versionInt() < 40000:
+ net_req.setAttribute(QNetworkRequest.FollowRedirectsAttribute, True)
+
request = QgsBlockingNetworkRequest()
error = request.get(net_req)
if error == QgsBlockingNetworkRequest.ErrorCode.NetworkError:
@@ -1168,7 +1171,7 @@ def _uri(self) -> QgsDataSourceUri:
)
return uri
- @log_function
+ @logger.log_function
def test_pg(self) -> bool:
""" Test the connection. """
uri = self._uri()
@@ -1180,7 +1183,7 @@ def test_pg(self) -> bool:
result = connection.executeSql("SELECT 1 AS lizmap_plugin_test")
except QgsProviderConnectionException:
# Credentials are wrong
- LOGGER.warning("Wrong credentials about the PostgreSQL database")
+ logger.warning("Wrong credentials about the PostgreSQL database")
else:
if len(result) >= 1:
self.currentPage().result_pg.setText(THUMBS)
@@ -1192,7 +1195,7 @@ def test_pg(self) -> bool:
self.currentPage().skip_db_label.setVisible(True)
return False
- @log_function
+ @logger.log_function
def save_pg(self) -> bool:
""" Save the current connection in the QGIS browser. """
name = self.field("pg_name")
@@ -1208,10 +1211,10 @@ def save_pg(self) -> bool:
return True
@staticmethod
- @log_function
+ @logger.log_function
def _save_pg(name: str, uri: QgsDataSourceUri) -> bool:
""" Save a PG connection from a URI. """
- LOGGER.info(
+ logger.info(
f"Create PG connection '{name}' : host {uri.host()}, database {uri.database()}, user {uri.username()}, pass XXXXX, port {uri.port()}"
)
config = {
diff --git a/lizmap/drag_drop_dataviz_manager.py b/lizmap/drag_drop_dataviz_manager.py
index e83eff34..d35b7bae 100644
--- a/lizmap/drag_drop_dataviz_manager.py
+++ b/lizmap/drag_drop_dataviz_manager.py
@@ -1,9 +1,4 @@
-from __future__ import annotations
-
-import logging
-
from enum import Enum, unique
-from typing import TYPE_CHECKING
from qgis.PyQt.QtCore import Qt
from qgis.PyQt.QtGui import QBrush, QIcon
@@ -19,13 +14,10 @@
QTreeWidgetItemIterator,
)
-from lizmap.toolbelt.i18n import tr
-from lizmap.toolbelt.resources import resources_path
-
-if TYPE_CHECKING:
- from lizmap.definitions.dataviz import DatavizDefinitions
-
-LOGGER = logging.getLogger('Lizmap')
+from . import logger
+from .definitions.dataviz import DatavizDefinitions
+from .toolbelt.i18n import tr
+from .toolbelt.resources import resources_path
@unique
@@ -302,7 +294,7 @@ def _container_from_cfg(self, data: list, parent: str | None = None) -> bool:
elif line['type'] == Container.Plot.value:
text, icon = self.metadata_from_uuid(line["uuid"])
if not icon:
- LOGGER.warning(
+ logger.warning(
"Plot having UUID '{}' was not found in the plot combobox, D&D panel, skipping this plot for "
"the drag&drop layout, only : {}.".format(
line["uuid"],
diff --git a/lizmap/logger.py b/lizmap/logger.py
index e8f8f390..35876a7e 100644
--- a/lizmap/logger.py
+++ b/lizmap/logger.py
@@ -1,26 +1,35 @@
import functools
-import logging
import time
-LOGGER = logging.getLogger('Lizmap')
-DEBUG = True
+from qgis.core import Qgis, QgsMessageLog
-# Re-export
+PLUGIN = "Lizmap"
+PROFILE = False
-debug = LOGGER.debug
-info = LOGGER.info
-warning = LOGGER.warning
-error = LOGGER.error
-critical = LOGGER.critical
+
+def info(message: str):
+ QgsMessageLog.logMessage(message, PLUGIN, Qgis.MessageLevel.Info)
+
+
+def warning(message: str):
+ QgsMessageLog.logMessage(message, PLUGIN, Qgis.MessageLevel.Warning)
+
+
+def critical(message: str):
+ QgsMessageLog.logMessage(message, PLUGIN, Qgis.MessageLevel.Critical)
+
+
+debug = info
+error = critical
def log_function(func):
""" Decorator to log function. """
@functools.wraps(func)
def log_function_core(*args, **kwargs):
- LOGGER.info(f"Calling function {func.__name__}")
+ info(f"Calling function {func.__name__}")
value = func(*args, **kwargs)
- LOGGER.info(f"End of function {func.__name__} with return : {value!s}")
+ info(f"End of function {func.__name__} with return : {value!s}")
return value
return log_function_core
@@ -34,7 +43,7 @@ def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
- LOGGER.info(f"{func.__name__} ran in {round(end - start, 2)}s")
+ info(f"{func.__name__} ran in {round(end - start, 2)}s")
return result
return wrapper
@@ -46,10 +55,7 @@ def log_output_value(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
- if DEBUG:
- LOGGER.info(f"{func.__name__} output is {result} for parameter {args!s}")
- else:
- LOGGER.info(f"{func.__name__} output is {result[0:200]}… for parameter {args!s}")
+ debug(f"{func.__name__} output is {result} for parameter {args!s}")
return result
return wrapper
diff --git a/lizmap/metadata.txt b/lizmap/metadata.txt
index b7cc6231..d04677e5 100644
--- a/lizmap/metadata.txt
+++ b/lizmap/metadata.txt
@@ -1,7 +1,6 @@
[general]
name=Lizmap
qgisMinimumVersion=3.34
-qgisMaximumVersion=3.99
author=3Liz
email=info@3liz.com
description=Publish and share your QGIS maps on the Web via Lizmap Web Client, by 3liz.com
diff --git a/lizmap/ogc_project_validity.py b/lizmap/ogc_project_validity.py
index 64997a3a..13b0b07f 100644
--- a/lizmap/ogc_project_validity.py
+++ b/lizmap/ogc_project_validity.py
@@ -1,11 +1,9 @@
-from __future__ import annotations
-
+"""
__copyright__ = 'Copyright 2023, 3Liz'
__license__ = 'GPL version 3'
__email__ = 'info@3liz.org'
-
+"""
import collections
-import logging
import re
from qgis.core import (
@@ -17,12 +15,11 @@
QgsProject,
)
-from lizmap.definitions.definitions import LayerProperties
-from lizmap.toolbelt.convert import cast_to_group, cast_to_layer
-from lizmap.toolbelt.layer import layer_property, set_layer_property
-from lizmap.toolbelt.strings import random_string, unaccent
-
-LOGGER = logging.getLogger('Lizmap')
+from . import logger
+from .definitions.definitions import LayerProperties
+from .toolbelt.convert import cast_to_group, cast_to_layer
+from .toolbelt.layer import layer_property, set_layer_property
+from .toolbelt.strings import random_string, unaccent
class OgcProjectValidity:
@@ -39,7 +36,7 @@ def add_shortnames(self):
existing, duplicated = self.existing_shortnames()
layer_tree = self.project.layerTreeRoot()
self._add_all_shortnames(layer_tree, existing, duplicated)
- LOGGER.info(f"New shortnames added : {len(self.new_shortnames_added)}")
+ logger.info(f"New shortnames added : {len(self.new_shortnames_added)}")
def _add_all_shortnames(self, layer_tree: QgsLayerTreeNode, existing_shortnames: list, duplicated: list):
""" Recursive function to add shortnames. """
@@ -54,7 +51,7 @@ def _add_all_shortnames(self, layer_tree: QgsLayerTreeNode, existing_shortnames:
new_shortname = self.short_name(source, existing_shortnames)
existing_shortnames.append(new_shortname)
set_layer_property(layer, LayerProperties.ShortName, new_shortname)
- LOGGER.info(f"New shortname added on layer '{layer.name()}' : {new_shortname}")
+ logger.info(f"New shortname added on layer '{layer.name()}' : {new_shortname}")
self.new_shortnames_added.append(new_shortname)
else:
child = cast_to_group(child)
@@ -72,7 +69,7 @@ def _add_all_shortnames(self, layer_tree: QgsLayerTreeNode, existing_shortnames:
else:
child.serverProperties().setShortName(new_shortname)
- LOGGER.info(f"New shortname added on group '{child.name()}' : {new_shortname}")
+ logger.info(f"New shortname added on group '{child.name()}' : {new_shortname}")
self.new_shortnames_added.append(new_shortname)
self._add_all_shortnames(child, existing_shortnames, duplicated)
@@ -80,7 +77,7 @@ def existing_shortnames(self) -> tuple[list[str], list[str]]:
""" Fetch all existing shortnames in the project. """
layer_tree = self.project.layerTreeRoot()
existing = self._read_all_shortnames(layer_tree, [])
- LOGGER.info('Existing shortnames detected before in project : ' + ', '.join(existing))
+ logger.info('Existing shortnames detected before in project : ' + ', '.join(existing))
duplicated = [item for item, count in collections.Counter(existing).items() if count > 1]
return existing, duplicated
@@ -124,7 +121,7 @@ def set_project_short_name(self):
root_layer_name = self.project.baseName()
project_short_name = self.short_name(root_layer_name, existing, 'p')
- LOGGER.info(f"Setting a project shortname : {project_short_name}")
+ logger.info(f"Setting a project shortname : {project_short_name}")
self.project.writeEntry("WMSRootName", "/", project_short_name)
@classmethod
diff --git a/lizmap/plugin/config.py b/lizmap/plugin/config.py
index 59d7f8c0..d4668fa8 100644
--- a/lizmap/plugin/config.py
+++ b/lizmap/plugin/config.py
@@ -322,7 +322,7 @@ def save_cfg_file(
self.project.layerTreeRoot(),
GroupNames.BaseLayers,
)
- if qgis_group and self.lwc_version >= LwcVersions.Lizmap_3_7:
+ if qgis_group is not None and self.lwc_version >= LwcVersions.Lizmap_3_7:
self.disable_legacy_empty_base_layer()
if self.version_checker:
diff --git a/lizmap/plugin/core.py b/lizmap/plugin/core.py
index e3a49eaa..5cfa4572 100644
--- a/lizmap/plugin/core.py
+++ b/lizmap/plugin/core.py
@@ -1,15 +1,8 @@
-from __future__ import annotations
-
import contextlib
import json
-import logging
-import os
-import tempfile
from functools import partial
from os.path import relpath
-from pathlib import Path
-from typing import TYPE_CHECKING
from qgis.core import (
Qgis,
@@ -23,6 +16,7 @@
QgsSettings,
QgsVectorLayer,
)
+from qgis.gui import QgisInterface
from qgis.PyQt.QtCore import (
QCoreApplication,
Qt,
@@ -107,32 +101,27 @@
QGIS_PLUGIN_MANAGER = False
-from lizmap.qt_style_sheets import NEW_FEATURE_CSS
-from lizmap.server_lwc import MAX_DAYS, ServerManager
-from lizmap.toolbelt.convert import ambiguous_to_bool, as_boolean
-from lizmap.toolbelt.custom_logging import (
- add_logging_handler_once,
- setup_logger,
-)
-from lizmap.toolbelt.git import current_git_hash, next_git_tag
-from lizmap.toolbelt.i18n import setup_translation, tr
-from lizmap.toolbelt.layer import (
+from .. import logger
+from ..qt_style_sheets import NEW_FEATURE_CSS
+from ..server_lwc import MAX_DAYS, ServerManager
+from ..toolbelt.convert import ambiguous_to_bool
+from ..toolbelt.git import current_git_hash, next_git_tag
+from ..toolbelt.i18n import setup_translation, tr
+from ..toolbelt.layer import (
layer_property,
)
-from lizmap.toolbelt.lizmap import convert_lizmap_popup
-from lizmap.toolbelt.resources import (
+from ..toolbelt.lizmap import convert_lizmap_popup
+from ..toolbelt.resources import (
load_icon,
- plugin_name,
plugin_path,
window_icon,
)
-from lizmap.toolbelt.version import (
+from ..toolbelt.version import (
qgis_version_info,
version,
)
-from lizmap.tooltip import Tooltip
-from lizmap.version_checker import VersionChecker
-
+from ..tooltip import Tooltip
+from ..version_checker import VersionChecker
from . import helpers
from .baselayers import BaseLayersManager
from .config import ConfigFileManager
@@ -146,10 +135,6 @@
from .training import TrainingManager
from .webdav import WebDavManager
-if TYPE_CHECKING:
- from qgis.gui import QgisInterface
-
-LOGGER = logging.getLogger(plugin_name())
VERSION_URL = "https://raw.githubusercontent.com/3liz/lizmap-web-client/versions/versions.json"
# To try a local file
# VERSION_URL = 'file:///home/etienne/.local/share/QGIS/QGIS3/profiles/default/Lizmap/released_versions.json'
@@ -178,7 +163,7 @@ def layerList(self) -> dict:
def __init__(self, iface: QgisInterface, lwc_version: LwcVersions = None):
"""Constructor of the Lizmap plugin."""
- LOGGER.info("Plugin starting")
+ logger.info("Plugin starting")
self.iface = iface
# noinspection PyArgumentList
self.project = QgsProject.instance()
@@ -191,10 +176,8 @@ def __init__(self, iface: QgisInterface, lwc_version: LwcVersions = None):
self.update_plugin = None
- setup_logger(plugin_name())
-
locale, file_path = setup_translation("lizmap_qgis_plugin_{}.qm", plugin_path("i18n"))
- LOGGER.info(f"Language in QGIS : {locale}")
+ logger.info(f"Language in QGIS : {locale}")
if file_path:
self.translator = QTranslator()
@@ -537,31 +520,6 @@ def __init__(self, iface: QgisInterface, lwc_version: LwcVersions = None):
self.help_action_cloud = None
def configure_dev_version(self):
- # File handler for logging
- temp_dir = Path(tempfile.gettempdir()).joinpath("QGIS_Lizmap")
- if not temp_dir.exists():
- temp_dir.mkdir()
-
- if not as_boolean(os.getenv("CI")):
- file_handler = logging.FileHandler(temp_dir.joinpath("lizmap.log"))
- file_handler.setLevel(logging.DEBUG)
- formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
- file_handler.setFormatter(formatter)
- add_logging_handler_once(LOGGER, file_handler)
- LOGGER.debug(
- f"The directory {temp_dir} "
- "is currently used for file logging."
- )
-
- # All logs
- def write_log_message(message, tag, level):
- """Write all tabs from QGIS to files."""
- temp_dir_log = Path(tempfile.gettempdir()).joinpath("QGIS_Lizmap")
- with open(temp_dir_log.joinpath("all.log"), "a") as log_file:
- log_file.write(f"{tag}({level}): {message}")
-
- QgsApplication.messageLog().messageReceived.connect(write_log_message)
-
self.dlg.setWindowTitle(
f"Lizmap branch {self.version}, commit {current_git_hash()}, next {next_git_tag()}"
)
@@ -645,7 +603,7 @@ def target_repository_changed(self):
def initGui(self):
"""Create action that will start plugin configuration"""
- LOGGER.debug("Plugin starting in the initGui")
+ logger.debug("Plugin starting in the initGui")
icon = window_icon()
self.action = QAction(icon, "Lizmap", self.iface.mainWindow())
@@ -1095,7 +1053,7 @@ def set_initial_extent_from_project(self):
self.dlg.widget_initial_extent.setOutputExtentFromUser(
extent, self.iface.mapCanvas().mapSettings().destinationCrs()
)
- LOGGER.info("Setting extent from the project")
+ logger.info("Setting extent from the project")
def remove_selected_layer_from_table(self, key):
"""
@@ -1104,7 +1062,7 @@ def remove_selected_layer_from_table(self, key):
"""
tw = self.layers_table[key]["tableWidget"]
tw.removeRow(tw.currentRow())
- LOGGER.info(f'Removing one row in table "{key}"')
+ logger.info(f'Removing one row in table "{key}"')
def remove_layer_from_table_by_layer_ids(self, layer_ids: list):
"""
@@ -1138,7 +1096,7 @@ def remove_layer_from_table_by_layer_ids(self, layer_ids: list):
if item_layer_id in layer_ids:
tw.removeRow(row)
- LOGGER.info(f'Layer ID "{layer_ids}" has been removed from the project')
+ logger.info(f'Layer ID "{layer_ids}" has been removed from the project')
def layout_renamed(self, layout: QgsMasterLayoutInterface, new_name: str):
"""When a layout has been renamed in the project."""
@@ -1232,7 +1190,7 @@ def configure_html_popup(self):
else:
text = ""
- LOGGER.info("Opening the popup configuration")
+ logger.info("Opening the popup configuration")
layer = self._current_selected_layer()
data = self.layer_options_list["popupSource"]["widget"].currentData()
@@ -1240,7 +1198,7 @@ def configure_html_popup(self):
# Legacy
# Lizmap HTML popup
if isinstance(layer, QgsVectorLayer):
- LOGGER.warning(
+ logger.warning(
"The 'lizmap' popup is deprecated for vector layer. This will be removed soon."
)
@@ -1257,7 +1215,7 @@ def configure_html_popup(self):
# Write the content into the global object
self.layerList[layer_or_group]["popupTemplate"] = content
if isinstance(layer, QgsVectorLayer):
- LOGGER.warning(
+ logger.warning(
"The 'lizmap' popup is deprecated for vector layer. This will be removed soon."
)
@@ -1340,7 +1298,7 @@ def maptip_from_form(self):
config = layer.editFormConfig()
# noinspection PyUnresolvedReferences
if config.layout() != QgsEditFormConfig.EditorLayout.TabLayout:
- LOGGER.warning("Maptip : the layer is not using a drag and drop form.")
+ logger.warning("Maptip : the layer is not using a drag and drop form.")
QMessageBox.warning(
self.dlg,
tr("Lizmap - Warning"),
@@ -1387,7 +1345,7 @@ def write_project_config_file(self, lwc_version: LwcVersions, with_gui: bool = T
with open(json_file, "w", encoding="utf8") as cfg_file:
cfg_file.write(json_file_content)
- LOGGER.info(
+ logger.info(
'The Lizmap configuration file has been written to "{path}"'.format(
path=json_file.absolute(),
)
diff --git a/lizmap/plugin/layer_tree.py b/lizmap/plugin/layer_tree.py
index fa0d30cb..deda5387 100644
--- a/lizmap/plugin/layer_tree.py
+++ b/lizmap/plugin/layer_tree.py
@@ -1,15 +1,14 @@
"""Layer tree panel configuration"""
-from __future__ import annotations
-
import contextlib
import hashlib
import json
import os
from typing import (
- TYPE_CHECKING,
Any,
+ Optional,
Protocol,
+ Tuple,
)
from qgis.core import (
@@ -26,6 +25,7 @@
QgsVectorLayer,
QgsWkbTypes,
)
+from qgis.gui import QgisInterface
from qgis.PyQt.QtCore import Qt
from qgis.PyQt.QtGui import QIcon
from qgis.PyQt.QtWidgets import (
@@ -34,6 +34,7 @@
)
from .. import logger
+from ..config import GlobalOptionsDefinitions, LayerOptionDefinitions
from ..definitions.definitions import (
DURATION_WARNING_BAR,
GroupNames,
@@ -56,12 +57,6 @@
)
from .helpers import display_error, string_to_list
-if TYPE_CHECKING:
- from qgis.gui import QgisInterface
-
- from ..config import GlobalOptionsDefinitions, LayerOptionDefinitions
- from ..dialogs.main import LizmapDialog
-
class LizmapProtocol(Protocol):
dlg: LizmapDialog
@@ -883,7 +878,7 @@ def _add_group_legend(
root_group = project.layerTreeRoot()
qgis_group = self.existing_group(root_group, label)
- if qgis_group:
+ if qgis_group is not None:
return qgis_group
new_group = root_group.addGroup(label)
@@ -892,16 +887,16 @@ def _add_group_legend(
return new_group
@staticmethod
- def existing_group(
- root_group: QgsLayerTree,
+ def _existing_group(
+ root_group: Optional[QgsLayerTree],
label: str,
index: bool = False,
- ) -> QgsLayerTreeGroup | int | None:
+ ) -> Optional[Tuple[QgsLayerTreeGroup, int]]:
"""Return the existing group in the legend if existing.
It will either return the group itself if found, or its index.
"""
- if not root_group:
+ if root_group is None:
return None
# Iterate over all child (layers and groups)
@@ -912,8 +907,7 @@ def existing_group(
i += 1
continue
- qgis_group = cast_to_group(child)
- qgis_group: QgsLayerTreeGroup
+ qgis_group: QgsLayerTreeGroup = cast_to_group(child)
count_children = len(qgis_group.children())
if count_children >= 1 or qgis_group.name() == label:
# We do not want to count empty groups
@@ -921,10 +915,26 @@ def existing_group(
i += 1
if qgis_group.name() == label:
- return i if index else qgis_group
+ return (qgis_group, i)
return None
+ @staticmethod
+ def existing_group(
+ root_group: Optional[QgsLayerTree],
+ label: str,
+ ) -> Optional[QgsLayerTreeGroup]:
+ g = LayerTreeManager._existing_group(root_group, label)
+ return g[0] if g is not None else None
+
+ @staticmethod
+ def existing_group_index(
+ root_group: Optional[QgsLayerTree],
+ label: str,
+ ) -> Optional[QgsLayerTreeGroup]:
+ g = LayerTreeManager._existing_group(root_group, label)
+ return g[1] if g is not None else None
+
def _add_base_layer(
self,
source: str,
diff --git a/lizmap/plugin/project.py b/lizmap/plugin/project.py
index bd7209cd..7ee7a47b 100644
--- a/lizmap/plugin/project.py
+++ b/lizmap/plugin/project.py
@@ -536,14 +536,13 @@ def project_config_file(
GroupNames.BaseLayers,
)
- if base_layers_group:
+ if base_layers_group is not None:
base_layers_group: QgsLayerTreeGroup
base_layers_group.setIsMutuallyExclusive(True, -1)
- default_background_color_index = LayerTreeManager.existing_group(
+ default_background_color_index = LayerTreeManager.existing_group_index(
base_layers_group,
GroupNames.BackgroundColor,
- index=True,
)
if default_background_color_index is not None and default_background_color_index >= 0:
diff --git a/lizmap/plugin_manager.py b/lizmap/plugin_manager.py
index c050ba6a..aa591596 100644
--- a/lizmap/plugin_manager.py
+++ b/lizmap/plugin_manager.py
@@ -1,28 +1,24 @@
-from __future__ import annotations
-
+"""
__copyright__ = 'Copyright 2022, 3Liz'
__license__ = 'GPL version 3'
__email__ = 'info@3liz.org'
-
-import logging
-
+"""
from collections import namedtuple
from pyplugin_installer import instance
from qgis.PyQt.QtCore import QDate, QDateTime, QLocale, Qt
from qgis.utils import iface
-from lizmap.definitions.definitions import DEV_VERSION_PREFIX
-from lizmap.server_lwc import ServerManager
-from lizmap.toolbelt.plugin import plugin_date
-from lizmap.toolbelt.version import version
+from . import logger
+from .definitions.definitions import DEV_VERSION_PREFIX
+from .server_lwc import ServerManager
+from .toolbelt.plugin import plugin_date
+from .toolbelt.version import version
Plugin = namedtuple('Plugin', ['name', 'version', 'date', 'template'])
DAYS_BEFORE_OUTDATED = 60
-LOGGER = logging.getLogger('Lizmap')
-
class QgisPluginManager:
@@ -80,26 +76,26 @@ def current_plugin_needs_update(self) -> bool | None:
current_version = version()
if current_version in DEV_VERSION_PREFIX:
# We trust developers
- LOGGER.debug("Version checker : in developers I trust")
+ logger.debug("Version checker : in developers I trust")
return False
current_version = ServerManager.split_lizmap_version(current_version)
if 'Lizmap' not in self.metadata:
# No QGIS plugin manager, nothing we can do now...
- LOGGER.debug("Version checker : NO QPM, nothing we can do now...")
+ logger.debug("Version checker : NO QPM, nothing we can do now...")
return False
latest_version = self.metadata['Lizmap'].version
if latest_version is None or latest_version == '':
- LOGGER.debug("Version checker : NO QPM, nothing we can do now...")
+ logger.debug("Version checker : NO QPM, nothing we can do now...")
return False
latest_version = ServerManager.split_lizmap_version(latest_version)
if current_version >= latest_version:
# Need to check this one if the previous check
# The current version is equal to the version in QGIS plugin manager
- LOGGER.warning(
+ logger.warning(
"Version checker : running a higher version than on plugins.qgis.org : "
"current {current} >= latest {latest}".format(
current='.'.join([str(i) for i in current_version]),
@@ -115,13 +111,13 @@ def current_plugin_needs_update(self) -> bool | None:
if not latest_date.isValid() or not current_plugin_date.isValid():
# We are missing some info, let's force them to update...
- LOGGER.debug("Version checker : Missing some dates, they should upgrade")
+ logger.debug("Version checker : Missing some dates, they should upgrade")
return True
# We are nice, we let them quite a lot of days to update
# Because we release a few versions per month
must_update = latest_date.daysTo(current_plugin_date) > DAYS_BEFORE_OUTDATED
- LOGGER.debug(f"Version checker : needs update : {must_update}")
+ logger.debug(f"Version checker : needs update : {must_update}")
return must_update
def lizmap_version(self):
diff --git a/lizmap/server_dav.py b/lizmap/server_dav.py
index 50064b80..4c4a5643 100644
--- a/lizmap/server_dav.py
+++ b/lizmap/server_dav.py
@@ -1,11 +1,6 @@
-from __future__ import annotations
-
-import logging
-
from base64 import b64encode
from collections import namedtuple
from pathlib import Path
-from typing import TYPE_CHECKING
from qgis.core import (
Qgis,
@@ -19,15 +14,12 @@
from qgis.PyQt.QtNetwork import QHttpMultiPart, QNetworkReply, QNetworkRequest
from qgis.PyQt.QtXml import QDomDocument
-from lizmap.definitions.definitions import RepositoryComboData, ServerComboData
-from lizmap.saas import webdav_properties
-from lizmap.toolbelt.i18n import tr
-from lizmap.toolbelt.strings import path_to_url
-
-if TYPE_CHECKING:
- from lizmap.dialogs.main import LizmapDialog
-
-LOGGER = logging.getLogger("Lizmap")
+from . import logger
+from .definitions.definitions import RepositoryComboData, ServerComboData
+from .dialogs.main import LizmapDialog
+from .saas import webdav_properties
+from .toolbelt.i18n import tr
+from .toolbelt.strings import path_to_url
PropFindFileResponse = namedtuple(
'PropFindFile',
@@ -166,11 +158,11 @@ def setup_webdav_dialog(self, dialog: LizmapDialog = None) -> bool:
# return False
if not self.dav_server:
- LOGGER.debug("Webdav is not installed")
+ logger.debug("Webdav is not installed")
return False
# If we have the webdav URL, it means the user have 'lizmap.webdav.access'
- LOGGER.debug(f"WebDAV is ready : {self.dav_server}")
+ logger.debug(f"WebDAV is ready : {self.dav_server}")
return True
def config_project(self):
@@ -186,10 +178,10 @@ def send_all_project_files(self) -> tuple[bool, str, str]:
if not url:
return False, '', ''
- LOGGER.info(f"SEND files to {url}")
+ logger.info(f"SEND files to {url}")
loop = QEventLoop()
- LOGGER.debug(f"Local path {self.qgs_path} to {url} with token {self.auth_id}")
+ logger.debug(f"Local path {self.qgs_path} to {url} with token {self.auth_id}")
self.qgs = self.webdav.store(self.qgs_path, url, self.auth_id, Qgis.ActionStart.Deferred)
self.qgs.stored.connect(loop.quit)
self.qgs.store()
@@ -197,11 +189,11 @@ def send_all_project_files(self) -> tuple[bool, str, str]:
error = self.qgs.errorString()
if error:
- LOGGER.error("Error while sending the QGS file : " + error)
+ logger.error("Error while sending the QGS file : " + error)
return False, error, ''
loop = QEventLoop()
- LOGGER.debug(f"Local path {self.cfg_path} to {url} with token {self.auth_id}")
+ logger.debug(f"Local path {self.cfg_path} to {url} with token {self.auth_id}")
self.cfg = self.webdav.store(self.cfg_path, url, self.auth_id, Qgis.ActionStart.Deferred)
self.cfg.stored.connect(loop.quit)
self.cfg.store()
@@ -209,11 +201,11 @@ def send_all_project_files(self) -> tuple[bool, str, str]:
error = self.cfg.errorString()
if error:
- LOGGER.error("Error while sending the Lizmap configuration file : " + error)
+ logger.error("Error while sending the Lizmap configuration file : " + error)
return False, error, ''
url = self.project_url()
- LOGGER.info(f"Project published on {url}")
+ logger.info(f"Project published on {url}")
return True, '', url
def send_thumbnail(self) -> tuple[bool, str]:
@@ -230,7 +222,7 @@ def send_thumbnail(self) -> tuple[bool, str]:
return False, ''
loop = QEventLoop()
- LOGGER.debug(f"Local path {self.thumbnail_path} to {url} with token {self.auth_id}")
+ logger.debug(f"Local path {self.thumbnail_path} to {url} with token {self.auth_id}")
self.thumbnail = self.webdav.store(str(self.thumbnail_path), url, self.auth_id, Qgis.ActionStart.Deferred)
self.thumbnail.stored.connect(loop.quit)
self.thumbnail.store()
@@ -238,7 +230,7 @@ def send_thumbnail(self) -> tuple[bool, str]:
error = self.thumbnail.errorString()
if error:
- LOGGER.error("Error while sending the thumbnail : " + error)
+ logger.error("Error while sending the thumbnail : " + error)
return False, error
return True, self.thumbnail_url()
@@ -257,7 +249,7 @@ def send_action(self) -> tuple[bool, str]:
return False, ''
loop = QEventLoop()
- LOGGER.debug(f"Local path {self.action_path} to {url} with token {self.auth_id}")
+ logger.debug(f"Local path {self.action_path} to {url} with token {self.auth_id}")
self.action = self.webdav.store(str(self.action_path), url, self.auth_id, Qgis.ActionStart.Deferred)
self.action.stored.connect(loop.quit)
self.action.store()
@@ -265,7 +257,7 @@ def send_action(self) -> tuple[bool, str]:
error = self.action.errorString()
if error:
- LOGGER.error("Error while sending the thumbnail : " + error)
+ logger.error("Error while sending the thumbnail : " + error)
return False, error
return True, ''
@@ -282,7 +274,7 @@ def send_media(self, file_path: Path) -> tuple[bool, str]:
url += 'media/'
loop = QEventLoop()
- LOGGER.debug(f"Local path {file_path} to {url} with token {self.auth_id}")
+ logger.debug(f"Local path {file_path} to {url} with token {self.auth_id}")
self.media = self.webdav.store(str(file_path), url, self.auth_id, Qgis.ActionStart.Deferred)
self.media.stored.connect(loop.quit)
self.media.store()
@@ -290,7 +282,7 @@ def send_media(self, file_path: Path) -> tuple[bool, str]:
error = self.media.errorString()
if error:
- LOGGER.error("Error while sending the media : " + error)
+ logger.error("Error while sending the media : " + error)
return False, error
return True, ''
@@ -386,7 +378,7 @@ def put_file(self, file_path: Path, remote_path: Path) -> tuple[bool, str]:
remote_server = self.dav_server + directory + path_to_url(remote_path)
loop = QEventLoop()
- LOGGER.debug(f"Local path {file_path} to {remote_server} with token {self.auth_id}")
+ logger.debug(f"Local path {file_path} to {remote_server} with token {self.auth_id}")
self.generic = self.webdav.store(str(file_path), remote_server, self.auth_id, Qgis.ActionStart.Deferred)
self.generic.stored.connect(loop.quit)
self.generic.store()
@@ -394,7 +386,7 @@ def put_file(self, file_path: Path, remote_path: Path) -> tuple[bool, str]:
error = self.generic.errorString()
if error:
- LOGGER.error("Error while sending the generic file : " + error)
+ logger.error("Error while sending the generic file : " + error)
return False, error
return True, ''
@@ -487,7 +479,7 @@ def put_file(self, file_path: Path, remote_path: Path) -> tuple[bool, str]:
# if reply.error() == QNetworkReply.ContentNotFoundError:
# return False, 'The file does not exist on the server.'
#
- # LOGGER.error(reply.errorString())
+ # logger.error(reply.errorString())
# return False, self.xml_reply_from_dav(reply)
def check_exists_qgs(self) -> tuple[bool, str | None]:
@@ -570,7 +562,7 @@ def file_stats(self, filename: str) -> tuple[PropFindFileResponse | None, str |
# Return None and empty error message
return None, ''
- LOGGER.error(reply.errorString())
+ logger.error(reply.errorString())
# Return None but try to parse the error message
return None, self.xml_reply_from_dav(reply)
diff --git a/lizmap/server_lwc.py b/lizmap/server_lwc.py
index 8c0c27a3..ab5797b8 100644
--- a/lizmap/server_lwc.py
+++ b/lizmap/server_lwc.py
@@ -1,7 +1,4 @@
-from __future__ import annotations
-
import json
-import logging
import os
import time
@@ -9,7 +6,6 @@
from functools import partial
from pathlib import Path
from typing import (
- TYPE_CHECKING,
Callable,
)
@@ -43,30 +39,25 @@
QWidget,
)
-from lizmap.definitions.definitions import (
+from . import logger
+from .definitions.definitions import (
DEV_VERSION_PREFIX,
UNSTABLE_VERSION_PREFIX,
ReleaseStatus,
ServerComboData,
)
-from lizmap.definitions.lizmap_cloud import CLOUD_QGIS_MIN_RECOMMENDED
-from lizmap.dialogs.server_wizard import (
+from .definitions.lizmap_cloud import CLOUD_QGIS_MIN_RECOMMENDED
+from .dialogs.main import LizmapDialog
+from .dialogs.server_wizard import (
CreateFolderWizard,
NamePage,
ServerWizard,
)
-from lizmap.saas import is_lizmap_cloud, webdav_properties
-from lizmap.toolbelt.convert import ambiguous_to_bool
-from lizmap.toolbelt.i18n import tr
-from lizmap.toolbelt.plugin import lizmap_user_folder, user_settings
-from lizmap.toolbelt.version import qgis_version_info, version
-
-if TYPE_CHECKING:
- from pathlib import Path
-
- from lizmap.dialogs.main import LizmapDialog
-
-LOGGER = logging.getLogger('Lizmap')
+from .saas import is_lizmap_cloud, webdav_properties
+from .toolbelt.convert import ambiguous_to_bool
+from .toolbelt.i18n import tr
+from .toolbelt.plugin import lizmap_user_folder, user_settings
+from .toolbelt.version import qgis_version_info, version
class TableCell(Enum):
@@ -237,16 +228,16 @@ def config_for_id(auth_id: str) -> QgsAuthMethodConfig | None:
""" Fetch the authentication settings for a given token. """
auth_manager = QgsApplication.authManager()
if not auth_manager.masterPasswordIsSet():
- LOGGER.warning(f"Master password is not set, could not look for ID {auth_id}")
+ logger.warning(f"Master password is not set, could not look for ID {auth_id}")
return None
conf = QgsAuthMethodConfig()
auth_manager.loadAuthenticationConfig(auth_id, conf, True)
if not conf.id():
- LOGGER.debug(f"Skipping password ID {auth_id}, it wasn't found in the password manager")
+ logger.debug(f"Skipping password ID {auth_id}, it wasn't found in the password manager")
return None
- # LOGGER.info("Found password ID {}".format(auth_id))
+ # logger.info("Found password ID {}".format(auth_id))
return conf
@classmethod
@@ -414,7 +405,7 @@ def remove_row(self):
QMessageBox.StandardButton.Ok)
self.table.clearSelection()
return
- LOGGER.debug(f"Row {auth_id} removed from the QGIS authentication database")
+ logger.debug(f"Row {auth_id} removed from the QGIS authentication database")
self.table.clearSelection()
self.table.removeRow(row)
@@ -645,7 +636,7 @@ def request_finished(self, row: int):
# Add the JSON metadata in the server combobox
index = self.server_combo.findData(url, ServerComboData.ServerUrl.value)
self.server_combo.setItemData(index, content, ServerComboData.JsonMetadata.value)
- LOGGER.info(f"Saving server metadata from network : {index} - {server_alias}")
+ logger.info(f"Saving server metadata from network : {index} - {server_alias}")
self.parent.tooltip_server_combo(index)
# Server combo is refreshed, maybe we can allow the menu bar
self.check_dialog_validity()
@@ -832,10 +823,10 @@ def refresh_server_combo(self):
with open(cache_file, encoding='utf8') as f:
metadata = json.load(f)
self.server_combo.setItemData(index, metadata, ServerComboData.JsonMetadata.value)
- LOGGER.info(f"Loading server {name} using cache in the drop down list")
+ logger.info(f"Loading server {name} using cache in the drop down list")
else:
self.server_combo.setItemData(index, {}, ServerComboData.JsonMetadata.value)
- LOGGER.info(
+ logger.info(
f"Loading server {name} without metadata in the drop down list")
self.parent.tooltip_server_combo(index)
@@ -1376,7 +1367,7 @@ def migrate_password_manager(self, servers: list):
if f'@{url}' in conf.name():
# Old format
- LOGGER.warning(f"Migrating the URL {url} in the QGIS authentication database")
+ logger.warning(f"Migrating the URL {url} in the QGIS authentication database")
user = conf.config('username')
password = conf.config('password')
@@ -1391,7 +1382,7 @@ def migrate_password_manager(self, servers: list):
result = auth_manager.storeAuthenticationConfig(config, True)
if not result:
- LOGGER.critical("Error while migrating the server")
+ logger.critical("Error while migrating the server")
# Other entries in the authentication database
for config in auth_manager.configIds():
@@ -1408,7 +1399,7 @@ def migrate_password_manager(self, servers: list):
split = conf.name().split('@')
if QUrl(split[-1]).isValid():
- LOGGER.critical(
+ logger.critical(
f"Is the password ID '{config}' in the QGIS authentication database a Lizmap server URL ? If yes, "
f"please remove it manually, otherwise skip this message. Go in the QGIS global properties, "
f"then 'Authentication' panel and check this ID."
diff --git a/lizmap/table_manager/base.py b/lizmap/table_manager/base.py
index 21655ffc..56985759 100644
--- a/lizmap/table_manager/base.py
+++ b/lizmap/table_manager/base.py
@@ -1,13 +1,10 @@
"""Table manager."""
-from __future__ import annotations
import inspect
import json
-import logging
import os
from collections import namedtuple
-from typing import TYPE_CHECKING
from qgis.core import QgsMapLayerModel, QgsMasterLayoutInterface, QgsProject
from qgis.PyQt.QtCore import Qt
@@ -21,18 +18,18 @@
QWidget,
)
-from lizmap.definitions.base import BaseDefinitions, InputType, InputTypeError
-from lizmap.definitions.dataviz import AggregationType, GraphType
-from lizmap.definitions.definitions import LwcVersions
-from lizmap.qt_style_sheets import NEW_FEATURE_CSS
-from lizmap.toolbelt.convert import as_boolean
-from lizmap.toolbelt.i18n import tr
-from lizmap.toolbelt.resources import plugin_name
-
-if TYPE_CHECKING:
- from lizmap.dialogs.main import LizmapDialog
-
-LOGGER = logging.getLogger(plugin_name())
+from .. import logger
+from ..definitions.base import (
+ BaseDefinitions,
+ InputType,
+ InputTypeError,
+)
+from ..definitions.dataviz import AggregationType, GraphType
+from ..definitions.definitions import LwcVersions
+from ..dialogs.main import LizmapDialog
+from ..qt_style_sheets import NEW_FEATURE_CSS
+from ..toolbelt.convert import as_boolean
+from ..toolbelt.i18n import tr
class CellError(Exception):
@@ -166,7 +163,7 @@ def __init__(
def set_lwc_version(self, current_version: LwcVersions):
""" When the target LWC version is changed, we need to update all widgets to set the color. """
- # LOGGER.debug("Set new LWC version {} in {}".format(current_version.value, self.definitions.key()))
+ # logger.debug("Set new LWC version {} in {}".format(current_version.value, self.definitions.key()))
found = False
for lwc_version in self.lwc_versions:
if found:
@@ -392,8 +389,9 @@ def _edit_row(self, row, data):
break
else:
msg = f'Error with value = "{value}" in list "{key}"'
- LOGGER.critical(msg)
+ logger.critical(msg)
raise InputTypeError(msg)
+
cell.setText(text)
if icon:
cell.setIcon(QIcon(icon))
@@ -526,7 +524,7 @@ def layers_has_been_deleted(self, layer_ids):
value = cell.data(Qt.ItemDataRole.UserRole)
if value == layer_id:
self.table.removeRow(i)
- LOGGER.info(f"Removing '{layer_id}' from table {self.definitions.key()}")
+ logger.info(f"Removing '{layer_id}' from table {self.definitions.key()}")
continue
def truncate(self):
@@ -705,7 +703,7 @@ def to_json(self, version: LwcVersions = None) -> dict:
if layer_data.get('type') == 'numeric':
if layer_data.get('end_field'):
# Incompatible with this format, but we don't remove it just in case
- LOGGER.error(
+ logger.error(
"A end_field is defined for the form filter. "
"This is not compatible for this version of Lizmap Web Client"
)
@@ -766,7 +764,7 @@ def to_json(self, version: LwcVersions = None) -> dict:
else:
key = layer_name
if result.get(layer_name):
- LOGGER.warning(
+ logger.warning(
f'Skipping "{layer_name}" while saving "{self.definitions.key()}" JSON configuration. Duplicated entry.')
result[key] = layer
result[key]['order'] = i
@@ -947,7 +945,7 @@ def from_json(self, data: dict):
if widget_type == InputType.Layer:
vector_layer = self.project.mapLayer(value)
if not vector_layer or not vector_layer.isValid():
- LOGGER.warning(
+ logger.warning(
f'In Lizmap configuration file, section "{self.definitions.key()}" with key {config_key}, the layer with ID "{value}" is '
'invalid or does not exist. Skipping that layer.')
else:
@@ -998,7 +996,7 @@ def from_json(self, data: dict):
if not vector_layer or not vector_layer.isValid():
# A layer temporary not available will be found in the project, but "not valid".
# Some metadata like CRS was still imported from the QGS file, but not fields
- LOGGER.warning(
+ logger.warning(
f'In Lizmap configuration file, section "{self.definitions.key()}", the layer with ID "{value}" is invalid or does '
'not exist. Trying to keep configuration.')
# Let's try to keep the configuration
@@ -1033,7 +1031,7 @@ def from_json(self, data: dict):
msg = (
f'Error with value = "{value}" in list "{key}", set default to {default_list_value}'
)
- LOGGER.warning(msg)
+ logger.warning(msg)
value = default_list_value
layer_data[key] = value
elif definition['type'] == InputType.SpinBox or definition['type'] == InputType.Text or definition['type'] == InputType.MultiLine or definition['type'] == InputType.HtmlWysiwyg or definition['type'] == InputType.Collection:
@@ -1055,7 +1053,7 @@ def from_json(self, data: dict):
layer_data[key] = default_value
else:
# raise InvalidCfgFile(')
- LOGGER.warning(
+ logger.warning(
f'In Lizmap configuration file, section "{self.definitions.key()}", one layer is missing the key "{key}" which is '
'mandatory. Skipping that layer.')
valid_layer = False
@@ -1064,7 +1062,7 @@ def from_json(self, data: dict):
if not valid_layer:
# We didn't find any valid layer during the process of reading this JSON dictionary
row = self.table.rowCount()
- LOGGER.info(
+ logger.info(
f"No valid layer found when reading this section {row + 1}. Not adding the row number {self.definitions.key()}"
)
continue
@@ -1077,7 +1075,7 @@ def from_json(self, data: dict):
# In CI, we still want to test this layer, sorry.
if vector_layer.dataProvider().name() != 'postgres':
- LOGGER.warning(
+ logger.warning(
f"The layer for editing {vector_layer.id()} is not stored in PostgreSQL. Now, only PostgreSQL layers "
"are supported for editing capabilities. Removing this layer from the "
"configuration.")
diff --git a/lizmap/table_manager/dataviz.py b/lizmap/table_manager/dataviz.py
index 4fb8301b..f6ed7801 100644
--- a/lizmap/table_manager/dataviz.py
+++ b/lizmap/table_manager/dataviz.py
@@ -1,10 +1,6 @@
""" Table manager for dataviz. """
-from __future__ import annotations
import json
-import logging
-
-from typing import TYPE_CHECKING
from qgis.core import (
QgsApplication,
@@ -27,20 +23,17 @@
from qgis.PyQt.QtWidgets import QAbstractButton, QDialog, QLabel, QWidget
from qgis.utils import OverrideCursor
-from lizmap.definitions.dataviz import GraphType
-from lizmap.definitions.definitions import ServerComboData
-from lizmap.dialogs.server_wizard import ServerWizard
-from lizmap.table_manager.base import TableManager
-from lizmap.toolbelt.convert import as_boolean
-from lizmap.toolbelt.i18n import tr
-from lizmap.toolbelt.resources import plugin_name, resources_path
-from lizmap.toolbelt.strings import merge_strings
-
-if TYPE_CHECKING:
- from lizmap.definitions.base import BaseDefinitions
- from lizmap.dialogs.main import LizmapDialog
-
-LOGGER = logging.getLogger(plugin_name())
+from .. import logger
+from ..definitions.base import BaseDefinitions
+from ..definitions.dataviz import GraphType
+from ..definitions.definitions import ServerComboData
+from ..dialogs.main import LizmapDialog
+from ..dialogs.server_wizard import ServerWizard
+from ..table_manager.base import TableManager
+from ..toolbelt.convert import as_boolean
+from ..toolbelt.i18n import tr
+from ..toolbelt.resources import resources_path
+from ..toolbelt.strings import merge_strings
class TableManagerDataviz(TableManager):
@@ -295,7 +288,7 @@ def dataviz_expression_filter(self, layer_id: str) -> str | None:
return None
if len(relations) >= 2:
- LOGGER.warning(
+ logger.warning(
f"Many relations has been found for the dataviz preview with the layer ID '{layer_id}'. "
"Only the first one is used."
)
diff --git a/lizmap/table_manager/layouts.py b/lizmap/table_manager/layouts.py
index 31b47915..ef1fd05d 100644
--- a/lizmap/table_manager/layouts.py
+++ b/lizmap/table_manager/layouts.py
@@ -1,24 +1,18 @@
""" Table manager for layouts. """
-from __future__ import annotations
-
-import logging
-
from enum import Enum
-from typing import TYPE_CHECKING
from qgis.core import QgsMasterLayoutInterface, QgsProject
from qgis.PyQt.QtCore import Qt
+from qgis.PyQt.QtWidgets import (
+ QAbstractButton,
+ QDialog,
+ QWidget,
+)
-from lizmap.definitions.definitions import LwcVersions
-from lizmap.table_manager.base import TableManager
-from lizmap.toolbelt.resources import plugin_name
-
-if TYPE_CHECKING:
- from qgis.PyQt.QtWidgets import QAbstractButton, QDialog, QWidget
-
- from lizmap.definitions.base import BaseDefinitions
-
-LOGGER = logging.getLogger(plugin_name())
+from .. import logger
+from ..definitions.base import BaseDefinitions
+from ..definitions.definitions import LwcVersions
+from .base import TableManager
class TableManagerLayouts(TableManager):
@@ -54,7 +48,7 @@ def label_dictionary_list() -> str:
def load_qgis_layouts(self, data: dict):
""" Load QGIS layouts into the table. """
- LOGGER.debug("Loading all layouts from the QGIS project :")
+ logger.debug("Loading all layouts from the QGIS project :")
tmp_layout_cfg = {}
if data:
for layout in data.get(self.label_dictionary_list()):
@@ -96,7 +90,7 @@ def load_qgis_layouts(self, data: dict):
for layout_name in ordered_names:
layout = qgis_layouts_by_name[layout_name]
# TODO check for report ?
- LOGGER.debug(f" * reading layout {layout.name()}")
+ logger.debug(f" * reading layout {layout.name()}")
row = self.table.rowCount()
self.table.setRowCount(row + 1)
@@ -175,7 +169,7 @@ def layout_renamed(self, layout: QgsMasterLayoutInterface, new_name: str):
value = cell.data(Qt.ItemDataRole.UserRole)
if value == old_name:
- LOGGER.info(f"Renaming layout from '{old_name}' to '{new_name}'")
+ logger.info(f"Renaming layout from '{old_name}' to '{new_name}'")
cell.setData(Qt.ItemDataRole.UserRole, new_name)
cell.setText(new_name)
break
diff --git a/lizmap/toolbelt/custom_logging.py b/lizmap/toolbelt/custom_logging.py
deleted file mode 100644
index 32998896..00000000
--- a/lizmap/toolbelt/custom_logging.py
+++ /dev/null
@@ -1,107 +0,0 @@
-"""Setting up logging using QGIS, file, Sentry..."""
-
-import logging
-
-from qgis.core import Qgis, QgsMessageLog
-
-from lizmap.toolbelt.i18n import tr
-from lizmap.toolbelt.resources import plugin_name
-
-PLUGIN_NAME = plugin_name()
-
-
-def qgis_level(logging_level):
- """Check for the corresponding QGIS Level according to Logging Level.
-
- For QGIS:
- https://qgis.org/api/classQgis.html#a60c079f4d8b7c479498be3d42ec96257
-
- For Logging:
- https://docs.python.org/3/library/logging.html#levels
-
- :param logging_level: The Logging level
- :type logging_level: basestring
-
- :return: The QGIS Level
- :rtype: Qgis.MessageLevel
- """
- if logging_level in ("CRITICAL", "ERROR"):
- return Qgis.MessageLevel.Critical
-
- if logging_level == "WARNING":
- return Qgis.MessageLevel.Warning
-
- return Qgis.MessageLevel.Info
-
-
-class QgsLogHandler(logging.Handler):
- """A logging handler that will log messages to the QGIS logging console."""
-
- def __init__(self, level=logging.NOTSET):
- logging.Handler.__init__(self)
-
- def emit(self, record):
- """Try to log the message to QGIS if available, otherwise do nothing.
-
- :param record: logging record containing whatever info needs to be
- logged.
- """
- try:
- QgsMessageLog.logMessage(record.getMessage(), PLUGIN_NAME, qgis_level(record.levelname))
- except MemoryError:
- message = tr(
- "Due to memory limitations on this machine, the plugin {} can not handle the full log"
- ).format(PLUGIN_NAME)
- print(message)
- QgsMessageLog.logMessage(message, PLUGIN_NAME, Qgis.MessageLevel.Critical)
-
-
-def add_logging_handler_once(logger, handler):
- """A helper to add a handler to a logger, ensuring there are no duplicates.
-
- :param logger: Logger that should have a handler added.
- :type logger: logging.logger
-
- :param handler: Handler instance to be added. It will not be added if an
- instance of that Handler subclass already exists.
- :type handler: logging.Handler
-
- :returns: True if the logging handler was added, otherwise False.
- :rtype: bool
- """
- class_name = handler.__class__.__name__
- for logger_handler in logger.handlers:
- if logger_handler.__class__.__name__ == class_name:
- return False
-
- logger.addHandler(handler)
- return True
-
-
-def setup_logger(logger_name):
- """Run once when the module is loaded and enable logging.
-
- :param logger_name: The logger name that we want to set up.
- :type logger_name: basestring
-
- Borrowed heavily from this:
- http://docs.python.org/howto/logging-cookbook.html
-
- Now to log a message do::
- LOGGER.debug('Some debug message')
- """
- logger = logging.getLogger(logger_name)
- logger.setLevel(logging.DEBUG)
-
- console_handler = logging.StreamHandler()
- console_handler.setLevel(logging.DEBUG)
- console_formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
- console_handler.setFormatter(console_formatter)
- add_logging_handler_once(logger, console_handler)
-
- qgis_handler = QgsLogHandler()
- qgis_formatter = logging.Formatter("%(levelname)s - %(message)s")
- qgis_handler.setFormatter(qgis_formatter)
- add_logging_handler_once(logger, qgis_handler)
-
- return logger
diff --git a/lizmap/toolbelt/debug.py b/lizmap/toolbelt/debug.py
deleted file mode 100644
index c349ada0..00000000
--- a/lizmap/toolbelt/debug.py
+++ /dev/null
@@ -1,12 +0,0 @@
-from qgis.PyQt.QtCore import Qt
-from qgis.PyQt.QtWidgets import QComboBox
-
-
-def _debug_combobox(combo: QComboBox, data_start: int = Qt.ItemDataRole.UserRole, data_max: int = 0):
- """Debug a QComboBox."""
- for i in range(combo.count()):
- print("=== NEW ITEM ===")
- print(combo.itemText(i))
- for x in range(data_max):
- print(f"→ {data_start + x} : {combo.itemData(data_start + x)}")
- print("==== END ITEM ====")
diff --git a/lizmap/tooltip.py b/lizmap/tooltip.py
index c2a312ba..9c2ec9dd 100644
--- a/lizmap/tooltip.py
+++ b/lizmap/tooltip.py
@@ -5,9 +5,6 @@
# Desktop lizmap/tooltip.py
# Server lizmap_server/tooltip.py
-from __future__ import annotations
-
-import logging
import re
from textwrap import dedent
@@ -25,7 +22,8 @@
from qgis.gui import QgsExternalResourceWidget
from qgis.PyQt.QtXml import QDomDocument
-LOGGER = logging.getLogger('Lizmap')
+from . import logger
+
SPACES = ' '
@@ -74,7 +72,7 @@ def create_popup_node_item_from_form(
if isinstance(node, QgsAttributeEditorField):
if node.idx() < 0:
# The form might have been imported from QML with some not existing fields
- LOGGER.warning(
+ logger.warning(
f'Layer {layer.id()} does not have a valid editor field')
return html
@@ -103,7 +101,7 @@ def create_popup_node_item_from_form(
if widget_type == 'ValueRelation':
if not QgsProject.instance().mapLayer(widget_config['Layer']):
# Issue #287
- LOGGER.warning(
+ logger.warning(
f'Layer {layer.id()} does not have a valid value relation layer for field {fname}')
return html
@@ -115,7 +113,7 @@ def create_popup_node_item_from_form(
if not referenced_layer:
# Issue #287
- LOGGER.warning(
+ logger.warning(
f'Layer {layer.id()} does not have a valid relation reference layer for field {fname}')
return html
@@ -139,7 +137,7 @@ def create_popup_node_item_from_form(
node.label(), relation.id(), relation.referencingLayerId())
else:
# Ticket https://github.com/3liz/qgis-lizmap-server-plugin/issues/82
- LOGGER.warning(
+ logger.warning(
f"The node '{node.name()}::{node.label()}' cannot be processed for the tooltip "
f"because the relation has not been found.")
diff --git a/lizmap/version_checker.py b/lizmap/version_checker.py
index af1ffe0f..5aab261c 100644
--- a/lizmap/version_checker.py
+++ b/lizmap/version_checker.py
@@ -1,25 +1,26 @@
+"""
__copyright__ = 'Copyright 2020, 3Liz'
__license__ = 'GPL version 3'
__email__ = 'info@3liz.org'
+"""
import json
-import logging
from qgis.core import Qgis, QgsNetworkContentFetcher
from qgis.PyQt.QtCore import QDate, QLocale, QUrl
-from lizmap.definitions.definitions import (
+from . import logger
+from .definitions.definitions import (
LwcVersions,
ReleaseStatus,
ServerComboData,
)
-from lizmap.definitions.online_help import current_locale
-from lizmap.dialogs.main import LizmapDialog
-from lizmap.dialogs.news import NewVersionDialog
-from lizmap.toolbelt.i18n import tr
-from lizmap.toolbelt.plugin import lizmap_user_folder
+from .definitions.online_help import current_locale
+from .dialogs.main import LizmapDialog
+from .dialogs.news import NewVersionDialog
+from .toolbelt.i18n import tr
+from .toolbelt.plugin import lizmap_user_folder
-LOGGER = logging.getLogger('Lizmap')
DAYS_BEING_OUTDATED = 90
@@ -55,7 +56,7 @@ def request_finished(self):
released_versions = json.loads(content)
except json.JSONDecodeError:
# Issue reported by rldhont by mail
- LOGGER.error(
+ logger.error(
"Error while reading the JSON file from Lizmap Web Client main repository, check the content with the "
"QGIS debug panel"
)
@@ -204,7 +205,7 @@ def check_outdated_version(self, lwc_version: LwcVersions, with_gui: True):
self.dialog.display_message_bar(title, description, Qgis.MessageLevel.Warning, 10, details)
return
- LOGGER.warning(
+ logger.warning(
f"This branch of Lizmap Web Client {lwc_version.value} is already outdated for more than {DAYS_BEING_OUTDATED} days. We encourage you "
f"to upgrade to the latest {self.newest_release_branch} or {self.oldest_release_branche}. A possible update of the plugin in a few months will remove "
"the support for writing the Lizmap configuration file to this version"
diff --git a/lizmap/widgets/html_editor.py b/lizmap/widgets/html_editor.py
index 42589742..03bcefa4 100644
--- a/lizmap/widgets/html_editor.py
+++ b/lizmap/widgets/html_editor.py
@@ -1,5 +1,4 @@
import json
-import logging
import os
import re
@@ -11,9 +10,10 @@
from qgis.PyQt.QtGui import QIcon
from qgis.PyQt.QtWidgets import QWidget
-from lizmap.toolbelt.convert import as_boolean
-from lizmap.toolbelt.i18n import tr
-from lizmap.toolbelt.resources import load_ui, resources_path
+from .. import logger
+from ..toolbelt.convert import as_boolean
+from ..toolbelt.i18n import tr
+from ..toolbelt.resources import load_ui, resources_path
WEBKIT_AVAILABLE = False
try:
@@ -43,7 +43,6 @@
FORM_CLASS = load_ui('ui_html_editor.ui')
-LOGGER = logging.getLogger('Lizmap')
# RegEx defined in QgsExpression.replaceExpressionText
# This function replaces each expression between [% and %] in the string
@@ -78,7 +77,7 @@ def __init__(self, parent):
self.web_view = QWebView()
else:
self.web_view = QgsCodeEditorHTML()
- LOGGER.warning(
+ logger.warning(
"WebKit is not available, falling back on the QGIS native plain HTML editor. Please upgrade your "
"set-up to have WebKit installed."
)
@@ -150,7 +149,7 @@ def set_html_content(self, content: str):
def _insert_qgis_expression(self, text: str):
""" Insert text at the current cursor position. """
- LOGGER.debug(f"Adding expression '{text}' in the HTML")
+ logger.debug(f"Adding expression '{text}' in the HTML")
self.insert_text(f'[% {text} %]')
def insert_text(self, text: str):
diff --git a/pyproject.toml b/pyproject.toml
index 766940c3..f0b7c08b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -11,6 +11,7 @@ license-files = ["LICENSE"]
readme = "README.md"
classifiers = [
"Operating System :: OS Independent",
+ "Programming Language :: Python :: 3.14",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.11",
@@ -28,11 +29,14 @@ repository ="https://github.com/3liz/lizmap-plugin"
[dependency-groups]
dev = [
+ "coverage[toml]",
{include-group = "tests"},
{include-group = "lint"},
]
tests = [
- "pytest >= 5.3",
+ "semver",
+ "pytest",
+ "pytest-qgis; python_full_version >= '3.10'",
"webdavclient3",
"psycopg[binary]",
]
@@ -46,9 +50,13 @@ lint = [
packaging = [
"qt-transifex; python_full_version >= '3.12'",
]
+tools = [
+ "ipython",
+]
[tool.uv.sources]
qt-transifex = { git = "https://github.com/3liz/qt-transifex" }
+pytest-qgis = { git = "https://github.com/3liz/pytest-qgis.git" }
[tool.yapt]
plugin_source = "lizmap"
@@ -81,7 +89,7 @@ indent-style = "space"
[tool.ruff.lint]
extend-select = [
"E", "F", "I", "ANN", "W", "T", "COM", "RUF",
- "C4", "SIM", "TC", "RET",
+ "C4", "SIM", "TC", "RET", "PTH"
]
# COM812 conflict with formatter
ignore = [
@@ -93,7 +101,8 @@ ignore = [
"SIM108", # Use of ternary operator
# TODO: Temporary ignore linter errors until
# they are fixed
- "T201",
+ "PTH", # Use pathlib
+ "T201", # print()
"E501", # Line too long
"RUF012", # Mutable class attributes should be annotated
"RUF067", # `__init__` module should only contain docstrings and re-exports
@@ -124,5 +133,9 @@ suppress-dummy-args = true
python_version = "3.9"
[[tool.mypy.overrides]]
-module = "qgis.*"
+module = [
+ "qgis.*",
+ "osgeo.*",
+ "processing.*",
+]
ignore_missing_imports = true
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 745d52ea..26d9d312 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -5,6 +5,7 @@ bandit==1.9.4
certifi==2026.5.20
charset-normalizer==3.4.7
colorama==0.4.6 ; sys_platform == 'win32'
+coverage==7.14.1
detect-secrets==1.5.0
exceptiongroup==1.3.1 ; python_full_version < '3.11'
idna==3.17
@@ -22,14 +23,16 @@ psycopg==3.3.4
psycopg-binary==3.3.4 ; implementation_name != 'pypy'
pygments==2.20.0
pytest==9.0.3
+pytest-qgis @ git+https://github.com/3liz/pytest-qgis.git@364633963a2d7b75aabde9044b8b79c3cd40c7ab
python-dateutil==2.9.0.post0
pyyaml==6.0.3
requests==2.34.2
rich==15.0.0
ruff==0.15.15
+semver==3.0.4
six==1.17.0
stevedore==5.8.0
-tomli==2.4.1 ; python_full_version < '3.11'
+tomli==2.4.1 ; python_full_version <= '3.11'
typing-extensions==4.15.0
tzdata==2026.2 ; sys_platform == 'win32'
urllib3==2.7.0
diff --git a/requirements/tests.txt b/requirements/tests.txt
index a243af6e..afd408b4 100644
--- a/requirements/tests.txt
+++ b/requirements/tests.txt
@@ -13,8 +13,10 @@ psycopg==3.3.4
psycopg-binary==3.3.4 ; implementation_name != 'pypy'
pygments==2.20.0
pytest==9.0.3
+pytest-qgis @ git+https://github.com/3liz/pytest-qgis.git@364633963a2d7b75aabde9044b8b79c3cd40c7ab
python-dateutil==2.9.0.post0
requests==2.34.2
+semver==3.0.4
six==1.17.0
tomli==2.4.1 ; python_full_version < '3.11'
typing-extensions==4.15.0 ; python_full_version < '3.13'
diff --git a/tests/conftest.py b/tests/conftest.py
index ffd8b830..f903a8c6 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,28 +1,30 @@
-import logging
import sys
from pathlib import Path
+from typing import Any
import pytest
-from qgis.core import Qgis, QgsApplication
-from qgis.PyQt import Qt
+from qgis.gui import QgisInterface
+from qgis.PyQt.QtCore import QT_VERSION_STR
-from .qgis_testing import start_app
+from .qgis_testing import QGIS_VERSION_INT, install_logger_hook, load_plugin
# with warnings.catch_warnings():
# warnings.filterwarnings("ignore", category=DeprecationWarning)
# from osgeo import gdal
+PLUGIN_SOURCE = "lizmap"
+
def pytest_report_header(config):
from osgeo import gdal
return (
- f"QGIS : {Qgis.versionInt()}\n"
+ f"QGIS : {QGIS_VERSION_INT}\n"
f"Python GDAL : {gdal.VersionInfo('VERSION_NUM')}\n"
f"Python : {sys.version}\n"
- f"QT : {Qt.QT_VERSION_STR}"
+ f"QT : {QT_VERSION_STR}"
)
@@ -31,6 +33,11 @@ def pytest_report_header(config):
#
+def pytest_sessionstart(session: pytest.Session):
+ """Start qgis application"""
+ install_logger_hook()
+
+
@pytest.fixture(scope="session")
def rootdir(request: pytest.FixtureRequest) -> Path:
return request.config.rootpath
@@ -41,40 +48,9 @@ def data(rootdir: Path) -> Path:
return rootdir.joinpath("data")
-#
-# Session
-#
-
-
-# Path the 'qgis.utils.iface' property
-# Which is not initialized when QGIS app
-# is initialized from testing module
-
-def pytest_sessionstart(session):
- """Start qgis application"""
- sys.path.append("/usr/share/qgis/python")
- start_app(session.path, False)
-
-#
-# Logger hook
-#
-
+@pytest.fixture(autouse=True, scope="session")
+def plugin(rootdir: Path, qgis_iface: QgisInterface, qgis_processing: Any) -> Any:
+ plugin_path = rootdir.parent.joinpath(PLUGIN_SOURCE)
+ plugin = load_plugin(plugin_path, qgis_iface, processing=False)
-def install_logger_hook(verbose: bool = False) -> None:
- """Install message log hook"""
- from qgis.core import Qgis
-
- # Add a hook to qgis message log
- def writelogmessage(message, tag, level):
- arg = f"{tag}: {message}"
- if level == Qgis.MessageLevel.Warning:
- logging.warning(arg)
- elif level == Qgis.MessageLevel.Critical:
- logging.error(arg)
- elif verbose:
- # Qgis is somehow very noisy
- # log only if verbose is set
- logging.info(arg)
-
- messageLog = QgsApplication.messageLog()
- messageLog.messageReceived.connect(writelogmessage)
+ yield plugin
diff --git a/tests/pytest.ini b/tests/pytest.ini
index 30afb5bf..9b20f014 100644
--- a/tests/pytest.ini
+++ b/tests/pytest.ini
@@ -1,6 +1,11 @@
[pytest]
log_cli=1
-log_cli_level=critical
+log_cli_level=error
+qgis_gui_enabled=false
+# For some reason, callinf exitQgis()
+# cause a segfault error when exiting
+# test session
+qgis_disable_exit=true
norecursedirs=
tests/data
.local
diff --git a/tests/qgis_testing.py b/tests/qgis_testing.py
index 6290515e..733cbe8f 100644
--- a/tests/qgis_testing.py
+++ b/tests/qgis_testing.py
@@ -1,127 +1,124 @@
+import configparser
+import importlib
import logging
-import os
-import shutil
import sys
from pathlib import Path
+from typing import (
+ Any,
+ Optional,
+)
-from qgis.core import QgsApplication
-
-
-# Path the 'qgis.utils.iface' property
-# Which is not initialized when QGIS app
-# is initialized from testing module
-def _patch_iface():
- import qgis.utils
-
- from qgis.testing.mocked import get_iface
- qgis.utils.iface = get_iface()
-
-
-# NOTE: we cannot use qgis.testing.start_app() directly
-# because it does not allow us to initialize the qgis settings
-# path as we want.
-def start_app(rootdir: Path, cleanup: bool = True):
- from qgis.PyQt.QtCore import QCoreApplication, Qt
-
- sys.path.append("/usr/share/qgis/python/plugins/") # for processing
-
- display = os.getenv("DISPLAY")
- if not display:
- os.environ["QT_QPA_PLATFORM"] = "offscreen"
-
- global QGISAPP
-
- QCoreApplication.setOrganizationName(QgsApplication.QGIS_ORGANIZATION_NAME)
- QCoreApplication.setOrganizationName(QgsApplication.QGIS_ORGANIZATION_DOMAIN)
- QCoreApplication.setApplicationName(QgsApplication.QGIS_APPLICATION_NAME)
-
- QCoreApplication.setAttribute(
- Qt.ApplicationAttribute.AA_ShareOpenGLContexts, True
- )
-
- load_qgis_settings(rootdir)
-
- # See https://github.com/python/mypy/issues/5732
- QGISAPP = QgsApplication( # type: ignore [name-defined]
- [],
- True,
- platformName="3liz-tests",
- )
-
- install_logger_hook(verbose=True)
-
- QGISAPP.initQgis() # type: ignore [name-defined]
- print(QGISAPP.showSettings()) # type: ignore [name-defined]
-
- # Patch 'iface' in qgis.utils
- _patch_iface()
-
- if cleanup:
- import atexit
-
- @atexit.register
- def exitQgis():
- QGISAPP.exitQgis()
-
-
-def init_processing():
- sys.path.append("/usr/share/qgis/python/plugins/")
-
- from processing.core.Processing import Processing
- Processing.initialize()
-
-
-def load_qgis_settings(rootdir: Path):
- from qgis.core import QgsSettings
- from qgis.PyQt.QtCore import QSettings
-
- path = rootdir.joinpath(".qgis-settings")
-
- os.environ["QGIS_CUSTOM_CONFIG_PATH"] = str(path)
- os.environ["QGIS_OPTIONS_PATH"] = str(path)
-
- settings_path = path.joinpath("profiles", "default")
-
- # Copy the ini file at correct location
- settings_file = settings_path.joinpath(
- QgsApplication.QGIS_ORGANIZATION_DOMAIN,
- "QGIS3.ini",
- )
- settings_file.parent.mkdir(parents=True, exist_ok=True)
-
- # Copy the ini file
- settings = rootdir.joinpath("qgis_settings.ini")
- if settings.exists():
- shutil.copyfile(settings, settings_file)
-
- QSettings.setDefaultFormat(QSettings.IniFormat)
- QSettings.setPath(QSettings.IniFormat, QSettings.UserScope, str(settings_path))
-
- qgssettings = QgsSettings()
- print("Settings loaded from ", qgssettings.fileName())
+import semver
+from qgis.core import Qgis, QgsApplication
+from qgis.gui import QgisInterface
+from qgis.server import QgsServerInterface
#
# Logger hook
#
+QGIS_VERSION_INT = Qgis.versionInt()
-def install_logger_hook(verbose: bool = False) -> None:
+
+def install_logger_hook() -> None:
"""Install message log hook"""
+ logging.info("Installing logger hook")
from qgis.core import Qgis
# Add a hook to qgis message log
- def writelogmessage(message, tag, level):
+ def writelogmessage(message, tag, level, *args):
arg = f"{tag}: {message}"
- if level == Qgis.MessageLevel.Warning:
+ if level == Qgis.Warning:
logging.warning(arg)
- elif level == Qgis.MessageLevel.Critical:
+ elif level == Qgis.Critical:
logging.error(arg)
- elif verbose:
- # Qgis is somehow very noisy
- # log only if verbose is set
+ else:
logging.info(arg)
messageLog = QgsApplication.messageLog()
- messageLog.messageReceived.connect(writelogmessage)
+ if QGIS_VERSION_INT < 40000:
+ messageLog.messageReceived.connect(writelogmessage)
+ else:
+ messageLog.messageReceivedWithFormat.connect(writelogmessage)
+
+#
+# Plugin loader
+#
+
+
+def load_plugin(
+ plugin_path: Path,
+ iface: Optional[QgisInterface] = None,
+ *,
+ processing: bool = False,
+) -> Any:
+ package = _load_plugin(plugin_path, processing=processing)
+ init = package.classFactory(iface)
+ if processing:
+ init.initProcessing()
+
+ return init
+
+
+def load_server_plugin(
+ plugin_path: Path,
+ iface: QgsServerInterface,
+ *,
+ processing: bool = False,
+) -> Any:
+ package = _load_plugin(plugin_path, processing=processing)
+ init = package.serverClassFactory(iface)
+ if processing:
+ init.initProcessing()
+
+ return init
+
+
+def _load_plugin(
+ plugin_path: Path,
+ *,
+ server: bool = False,
+ processing: bool = False,
+) -> Any:
+ logging.info("Loading plugin: %s", plugin_path)
+ cp = configparser.ConfigParser()
+ with plugin_path.joinpath("metadata.txt").open() as f:
+ cp.read_file(f)
+ if server:
+ assert cp["server"].getboolean("server"), "Server plugin required"
+ if processing:
+ assert cp["general"].getboolean("hasProcessingProvider"), "Processing plugin required"
+ assert _check_qgis_version(
+ cp["general"].get("qgisMinimumVersion"),
+ cp["general"].get("qgisMaximumVersion"),
+ ), f"Qgis version {Qgis.version()} not supported"
+
+ sys.path.append(str(plugin_path.parent))
+
+ plugin = plugin_path.name
+
+ package = importlib.import_module(plugin)
+ assert plugin in sys.modules
+
+ return package
+
+
+def _check_qgis_version(minver: Optional[str], maxver: Optional[str]) -> bool:
+ version = semver.Version.parse(Qgis.QGIS_VERSION.split("-", maxsplit=1)[0])
+
+ def _version(ver: Optional[str]) -> semver.Version:
+ if not ver:
+ return version
+
+ # Normalize version
+ parts = ver.split(".")
+ match len(parts):
+ case 1:
+ parts.extend(("0", "0"))
+ case 2:
+ parts.append("0")
+ return semver.Version.parse(".".join(parts))
+
+ return _version(minver) <= version <= _version(maxver)
diff --git a/tests/test_layer_tree.py b/tests/test_layer_tree.py
index 5d87b05d..924b1262 100644
--- a/tests/test_layer_tree.py
+++ b/tests/test_layer_tree.py
@@ -3,7 +3,7 @@
from pathlib import Path
from qgis.core import Qgis, QgsProject, QgsVectorLayer
-from qgis.testing.mocked import get_iface
+from qgis.gui import QgisInterface
from lizmap.definitions.definitions import LayerProperties, LwcVersions
from lizmap.plugin import Lizmap
@@ -57,7 +57,7 @@ def test_string_to_list(self):
self.assertListEqual(string_to_list(" a "), ["a"])
self.assertListEqual(string_to_list("a,b"), ["a", "b"])
- def test_layer_metadata(self, data: Path):
+ def test_layer_metadata(self, data: Path, qgis_iface: QgisInterface):
"""Test metadata coming from layer or from Lizmap."""
project = QgsProject.instance()
layer_name = "lines"
@@ -68,7 +68,7 @@ def test_layer_metadata(self, data: Path):
lizmap_config_url = "https://lizmap.url"
qgis_config_url = "https://qgis.url"
- lizmap = Lizmap(get_iface(), lwc_version=LwcVersions.latest())
+ lizmap = Lizmap(qgis_iface, lwc_version=LwcVersions.latest())
# New project so Lizmap is empty
config = lizmap.layers_config_file()
diff --git a/tests/test_ui.py b/tests/test_ui.py
index b38e2eab..afa7805e 100644
--- a/tests/test_ui.py
+++ b/tests/test_ui.py
@@ -1,15 +1,14 @@
"""Test Lizmap dialog UI."""
import json
-import logging
from pathlib import Path
import pytest
from qgis.core import QgsProject, QgsRasterLayer, QgsVectorLayer
+from qgis.gui import QgisInterface
from qgis.PyQt.QtCore import Qt
-from qgis.testing.mocked import get_iface
from lizmap.definitions.definitions import LwcVersions, PredefinedGroup
from lizmap.plugin import Lizmap
@@ -26,849 +25,841 @@ def teardown(data: Path) -> None:
filepath.unlink()
-class TestUiLizmapDialog(TestCase):
- def test_ui_base(self, data: Path):
- """Test opening the Lizmap dialog with some basic checks."""
- project = QgsProject.instance()
- project.clear()
- lizmap = Lizmap(get_iface(), lwc_version=LwcVersions.latest())
-
- layer = QgsVectorLayer(str(data.joinpath("lines.geojson")), "lines", "ogr")
- project.addMapLayer(layer)
-
- layer = QgsVectorLayer(str(data.joinpath("points.geojson")), "points", "ogr")
- project.addMapLayer(layer)
-
- flag, message = lizmap.check_global_project_options()
- self.assertFalse(flag, message)
- self.assertEqual(
- message,
- "You need to open a QGIS project, using the QGS extension.
This is needed before using other tabs in "
- "the plugin.",
- )
-
- project.write(str(data.joinpath("unittest.qgs")))
- flag, message = lizmap.check_global_project_options()
- self.assertTrue(flag, message)
-
- # lizmap.run()
- # lizmap.get_map_options()
-
- def test_legend_options(self, data: Path):
- """Test about reading legend options."""
- project = QgsProject.instance()
- project.read(str(data.joinpath("legend_image_option.qgs")))
- self.assertEqual(3, len(project.mapLayers()))
-
- lizmap = Lizmap(get_iface(), lwc_version=LwcVersions.latest())
- # read_cfg_file will call "layers_config_file"
- config = lizmap.read_cfg_file(skip_tables=True)
- print("\n::test_legend_options::config", config)
-
- lizmap.process_node(lizmap.layerList, project.layerTreeRoot(), None, config)
-
- self.assertEqual("5000", lizmap.dlg.minimum_scale.text())
- self.assertEqual("500000", lizmap.dlg.maximum_scale.text())
- self.assertEqual("5000, 250000, 500000", lizmap.dlg.list_map_scales.text())
-
- self.assertEqual("disabled", lizmap.layerList.get("legend_disabled_layer_id").get("legend_image_option"))
-
- self.assertEqual(
- "expand_at_startup",
- lizmap.layerList.get("legend_displayed_startup_layer_id").get("legend_image_option"),
- )
-
- self.assertEqual(
- "hide_at_startup", lizmap.layerList.get("legend_hidden_startup_layer_id").get("legend_image_option")
- )
-
- # For LWC 3.6
- output = lizmap.project_config_file(
- LwcVersions.Lizmap_3_6,
- check_server=False,
- ignore_error=True,
- )
-
- # NOTE: Seems that HTML widget not working in tests
- # See lizmap.widgets.html_editor line 117
- logging.warning("HTML widget not working in tests")
- # self.assertTrue('
' in output['options']['datavizTemplate'])
-
- self.assertEqual(
- output["layers"]["legend_displayed_startup"]["legend_image_option"], "expand_at_startup"
- )
- self.assertIsNone(output["layers"]["legend_displayed_startup"].get("noLegendImage"))
-
- self.assertIsNone(output["options"].get("default_background_color_index"))
-
- # For LWC 3.5
- output = lizmap.project_config_file(
- LwcVersions.Lizmap_3_5, with_gui=False, check_server=False, ignore_error=True
- )
- self.assertIsNone(output["layers"]["legend_displayed_startup"].get("legend_image_option"))
- self.assertEqual(output["layers"]["legend_displayed_startup"]["noLegendImage"], str(False))
-
- def _setup_empty_project(
- self,
- data: Path,
- lwc_version: LwcVersions = LwcVersions.latest(),
- ) -> Lizmap:
- """Internal function to add a layer and a basic check."""
- project = QgsProject.instance()
- layer = QgsVectorLayer(str(data.joinpath("lines.geojson")), "lines", "ogr")
- project.addMapLayer(layer)
- project.setFileName(temporary_file_path())
-
- lizmap = Lizmap(get_iface(), lwc_version=lwc_version)
- baselayers = lizmap._add_group_legend("baselayers", exclusive=True, parent=None, project=project)
- lizmap._add_group_legend(
- "project-background-color", exclusive=False, parent=baselayers, project=project
- )
- hidden = lizmap._add_group_legend("hidden", project=project)
-
- # For testing, we add OSM as hidden layer
- hidden_raster = QgsRasterLayer(
- "type=xyz&url=https://tile.openstreetmap.org/{z}/{x}/{y}.png", "OSM", "wms"
- )
- project.addMapLayer(hidden_raster, False)
- hidden.addLayer(hidden_raster)
-
- # Do not use read_lizmap_config_file
- # as it will be called by read_cfg_file and also the UI is set in read_cfg_file
- config = lizmap.read_cfg_file(skip_tables=True)
-
- lizmap.dlg.widget_initial_extent.setOutputExtentFromLayer(layer)
-
- # Config is empty in the CFG file because it's a new project
- self.assertDictEqual({}, config)
-
- # Some process
- lizmap.process_node(lizmap.layerList, project.layerTreeRoot(), None, {})
-
- return lizmap
-
- def test_lizmap_layer_properties(self, data: Path):
- """Test apply some properties in a layer in the dialog."""
- lizmap = self._setup_empty_project(data)
-
- # Click the layer
- item = lizmap.dlg.layer_tree.topLevelItem(0)
- self.assertEqual(item.text(0), "lines")
- self.assertTrue(item.text(1).startswith("lines_"))
- self.assertEqual(item.text(2), "layer")
- self.assertEqual(item.data(0, Qt.ItemDataRole.UserRole + 1), PredefinedGroup.No.value)
- self.assertEqual(item.text(3), "") # Not used, just to test
-
- self.assertFalse(lizmap.dlg.list_group_visibility.isEnabled())
-
- # Click the first line
- lizmap.dlg.layer_tree.setCurrentItem(lizmap.dlg.layer_tree.topLevelItem(0))
-
- # Fill the ACL field
- self.assertTrue(lizmap.dlg.list_group_visibility.isEnabled())
- acl_layer = "a_group_id"
- lizmap.dlg.list_group_visibility.setText(acl_layer)
- lizmap.save_value_layer_group_data("group_visibility")
-
- # Fill the abstract field
- html_abstract = "Hello"
- lizmap.dlg.teLayerAbstract.setPlainText(html_abstract)
- lizmap.save_value_layer_group_data("abstract")
-
- # Click the group base-layers
- group_item = lizmap.dlg.layer_tree.findItems(
- "baselayers", Qt.MatchFlag.MatchContains | Qt.MatchFlag.MatchRecursive, 0
- )[0]
- lizmap.dlg.layer_tree.setCurrentItem(group_item)
- self.assertFalse(lizmap.dlg.panel_layer_all_settings.isEnabled())
-
- # Click the group project-background-color
- group_item = lizmap.dlg.layer_tree.findItems(
- "project-background-color", Qt.MatchFlag.MatchContains | Qt.MatchFlag.MatchRecursive, 0
- )[0]
- lizmap.dlg.layer_tree.setCurrentItem(group_item)
- self.assertTrue(lizmap.dlg.panel_layer_all_settings.isEnabled())
- self.assertTrue(lizmap.dlg.group_layer_metadata.isEnabled())
- self.assertFalse(lizmap.dlg.group_layer_tree_options.isEnabled())
-
- # Click the group hidden
- group_item = lizmap.dlg.layer_tree.findItems(
- "hidden", Qt.MatchFlag.MatchContains | Qt.MatchFlag.MatchRecursive, 0
- )[0]
- lizmap.dlg.layer_tree.setCurrentItem(group_item)
- # It should work, maybe the test click and click in the UI is missing one thing
- # self.assertEqual(PredefinedGroup.Hidden.value, lizmap._current_item_predefined_group())
- # self.assertFalse(lizmap.dlg.panel_layer_all_settings.isEnabled())
-
- # Back to a layer outside of these groups
- group_item = lizmap.dlg.layer_tree.findItems(
- "lines", Qt.MatchFlag.MatchContains | Qt.MatchFlag.MatchRecursive, 0
- )[0]
- lizmap.dlg.layer_tree.setCurrentItem(group_item)
- self.assertTrue(lizmap.dlg.list_group_visibility.isEnabled())
-
- # Check new values in the output config
- output = lizmap.project_config_file(
- LwcVersions.latest(),
- check_server=False,
- ignore_error=True,
- )
-
- # Layers
- self.assertListEqual(output["layers"]["lines"]["group_visibility"], [acl_layer])
- self.assertEqual(output["layers"]["lines"]["abstract"], html_abstract)
- # Predefined groups, still in the CFG
- self.assertListEqual(output["layers"]["baselayers"]["group_visibility"], [])
- self.assertEqual(output["layers"]["baselayers"]["abstract"], "")
- self.assertListEqual(output["layers"]["project-background-color"]["group_visibility"], [])
- self.assertEqual(output["layers"]["project-background-color"]["abstract"], "")
- self.assertEqual(output["options"].get("default_background_color_index"), 0)
-
- self.assertFalse(output["layers"]["lines"].get("children_lizmap_features_table"))
- self.assertEqual("False", output["layers"]["lines"].get("popupDisplayChildren"))
-
- # Test a false value as a string which shouldn't be there by default
- self.assertIsNone(output["layers"]["lines"].get("externalWmsToggle"))
- self.assertIsNone(output["layers"]["lines"].get("metatileSize"))
-
- def test_default_options_values_3_6(self, data: Path):
- """Test default options values."""
- lizmap = self._setup_empty_project(data)
-
- output = lizmap.project_config_file(LwcVersions.Lizmap_3_6, check_server=False, ignore_error=True)
-
- # generic options
- self.assertIsNone(output["options"].get("hideProject"))
- self.assertFalse(output["options"].get("automatic_permalink"))
- self.assertFalse(output["options"].get("wms_single_request_for_all_layers"))
- self.assertIsNone(output["options"].get("acl"))
-
- # map tools
- self.assertFalse(output["options"].get("measure"))
- self.assertFalse(output["options"].get("print")) # The checkbox is removed since LWC 3.7.0
- self.assertFalse(output["options"].get("zoomHistory")) # The checkbox is removed since LWC 3.8.0
- self.assertFalse(output["options"].get("geolocation"))
- self.assertFalse(output["options"].get("draw"))
- self.assertIsNone(output["options"].get("externalSearch"))
- self.assertEqual(25, output["options"].get("pointTolerance"))
- self.assertEqual(10, output["options"].get("lineTolerance"))
- self.assertEqual(5, output["options"].get("polygonTolerance"))
-
- # API keys
- self.assertIsNone(output["options"].get("googleKey"))
- self.assertIsNone(output["options"].get("bingKey"))
- self.assertIsNone(output["options"].get("ignKey"))
-
- # Scales
- self.assertFalse(output["options"].get("use_native_zoom_levels"))
- self.assertFalse(output["options"].get("hide_numeric_scale_value"))
- self.assertEqual([10000, 25000, 50000, 100000, 250000, 500000], output["options"].get("mapScales"))
- self.assertEqual(1, output["options"].get("minScale"))
- self.assertEqual(1000000000, output["options"].get("maxScale"))
- self.assertIsNone(output["options"].get("max_scale_points"))
- self.assertIsNone(output["options"].get("max_scale_lines_polygons"))
-
- # Map interface
- self.assertFalse(output["options"].get("hideHeader"))
- self.assertFalse(output["options"].get("hideMenu"))
- self.assertFalse(output["options"].get("hideLegend"))
- self.assertFalse(output["options"].get("hideOverview"))
- self.assertFalse(output["options"].get("hideNavbar"))
- self.assertEqual("dock", output["options"].get("popupLocation"))
- self.assertTrue(output["options"].get("fixed_scale_overview_map"))
-
- # Layers page
- self.assertIsNone(output["options"].get("hideGroupCheckbox"))
- self.assertIsNone(output["options"].get("activateFirstMapTheme"))
-
- # Baselayers page
- self.assertIsNone(output["options"].get("emptyBaselayer"))
- self.assertIsNone(output["options"].get("startupBaselayer"))
-
- # Attribute page
- self.assertIsNone(output["options"].get("limitDataToBbox"))
-
- # Layouts page
- self.assertFalse(output["options"].get("default_popup_print"))
-
- # Dataviz page
- self.assertIsNone(output["options"].get("datavizTemplate"))
- self.assertIsNone(output["options"].get("dataviz_drag_drop"))
- self.assertEqual("dock", output["options"].get("datavizLocation"))
- self.assertIsNone(output["options"].get("theme")) # default value "dark" is not set
-
- # Time manager page
- self.assertEqual(10, output["options"].get("tmTimeFrameSize"))
- self.assertEqual("seconds", output["options"].get("tmTimeFrameType"))
- self.assertEqual(1000, output["options"].get("tmAnimationFrameLength"))
-
- # Atlas page
- self.assertIsNone(output["options"].get("atlasShowAtStartup"))
- self.assertIsNone(output["options"].get("atlasAutoPlay"))
-
- def test_default_options_values_3_7(self, data: Path):
- """Test default options values."""
- lizmap = self._setup_empty_project(data)
-
- output = lizmap.project_config_file(LwcVersions.Lizmap_3_7, check_server=False, ignore_error=True)
-
- # generic options
- self.assertIsNone(output["options"].get("hideProject"))
- self.assertFalse(output["options"].get("automatic_permalink"))
- self.assertFalse(output["options"].get("wms_single_request_for_all_layers"))
- self.assertIsNone(output["options"].get("acl"))
-
- # map tools
- self.assertFalse(output["options"].get("measure"))
- self.assertIsNone(output["options"].get("print")) # The checkbox is removed since LWC 3.7.0
- self.assertFalse(output["options"].get("zoomHistory")) # The checkbox is removed since LWC 3.8.0
- self.assertFalse(output["options"].get("geolocation"))
- self.assertFalse(output["options"].get("draw"))
- self.assertIsNone(output["options"].get("externalSearch"))
- self.assertEqual(25, output["options"].get("pointTolerance"))
- self.assertEqual(10, output["options"].get("lineTolerance"))
- self.assertEqual(5, output["options"].get("polygonTolerance"))
-
- # API keys
- self.assertIsNone(output["options"].get("googleKey"))
- self.assertIsNone(output["options"].get("bingKey"))
- self.assertIsNone(output["options"].get("ignKey"))
-
- # Scales
- self.assertFalse(output["options"].get("use_native_zoom_levels"))
- self.assertFalse(output["options"].get("hide_numeric_scale_value"))
- self.assertEqual([10000, 25000, 50000, 100000, 250000, 500000], output["options"].get("mapScales"))
- self.assertEqual(1, output["options"].get("minScale"))
- self.assertEqual(1000000000, output["options"].get("maxScale"))
- self.assertIsNone(output["options"].get("max_scale_points"))
- self.assertIsNone(output["options"].get("max_scale_lines_polygons"))
-
- # Map interface
- self.assertFalse(output["options"].get("hideHeader"))
- self.assertFalse(output["options"].get("hideMenu"))
- self.assertFalse(output["options"].get("hideLegend"))
- self.assertFalse(output["options"].get("hideOverview"))
- self.assertFalse(output["options"].get("hideNavbar"))
- self.assertEqual("dock", output["options"].get("popupLocation"))
- self.assertTrue(output["options"].get("fixed_scale_overview_map"))
-
- # Layers page
- self.assertIsNone(output["options"].get("hideGroupCheckbox"))
- self.assertIsNone(output["options"].get("activateFirstMapTheme"))
-
- # Baselayers page
- self.assertIsNone(output["options"].get("emptyBaselayer"))
- self.assertIsNone(output["options"].get("startupBaselayer"))
-
- # Attribute page
- self.assertIsNone(output["options"].get("limitDataToBbox"))
-
- # Layouts page
- self.assertFalse(output["options"].get("default_popup_print"))
-
- # Dataviz page
- self.assertIsNone(output["options"].get("datavizTemplate"))
- self.assertIsNone(output["options"].get("dataviz_drag_drop"))
- self.assertEqual("dock", output["options"].get("datavizLocation"))
- self.assertIsNone(output["options"].get("theme")) # default value "dark" is not set
-
- # Time manager page
- self.assertEqual(10, output["options"].get("tmTimeFrameSize"))
- self.assertEqual("seconds", output["options"].get("tmTimeFrameType"))
- self.assertEqual(1000, output["options"].get("tmAnimationFrameLength"))
-
- # Atlas page
- self.assertIsNone(output["options"].get("atlasShowAtStartup"))
- self.assertIsNone(output["options"].get("atlasAutoPlay"))
-
- def test_default_options_values_3_8(self, data: Path):
- """Test default options values."""
- lizmap = self._setup_empty_project(data)
-
- output = lizmap.project_config_file(LwcVersions.Lizmap_3_8, check_server=False, ignore_error=True)
-
- # generic options
- self.assertIsNone(output["options"].get("hideProject"))
- self.assertFalse(output["options"].get("automatic_permalink"))
- self.assertFalse(output["options"].get("wms_single_request_for_all_layers"))
- self.assertIsNone(output["options"].get("acl"))
-
- # map tools
- self.assertFalse(output["options"].get("measure"))
- self.assertIsNone(output["options"].get("print")) # The checkbox is removed since LWC 3.7.0
- self.assertIsNone(output["options"].get("zoomHistory")) # The checkbox is removed since LWC 3.8.0
- self.assertFalse(output["options"].get("geolocation"))
- self.assertFalse(output["options"].get("draw"))
- self.assertIsNone(output["options"].get("externalSearch"))
- self.assertEqual(25, output["options"].get("pointTolerance"))
- self.assertEqual(10, output["options"].get("lineTolerance"))
- self.assertEqual(5, output["options"].get("polygonTolerance"))
-
- # API keys
- self.assertIsNone(output["options"].get("googleKey"))
- self.assertIsNone(output["options"].get("bingKey"))
- self.assertIsNone(output["options"].get("ignKey"))
-
- # Scales
- self.assertFalse(output["options"].get("use_native_zoom_levels"))
- self.assertFalse(output["options"].get("hide_numeric_scale_value"))
- self.assertEqual([10000, 25000, 50000, 100000, 250000, 500000], output["options"].get("mapScales"))
- self.assertEqual(1, output["options"].get("minScale"))
- self.assertEqual(1000000000, output["options"].get("maxScale"))
- self.assertIsNone(output["options"].get("max_scale_points"))
- self.assertIsNone(output["options"].get("max_scale_lines_polygons"))
-
- # Map interface
- self.assertFalse(output["options"].get("hideHeader"))
- self.assertFalse(output["options"].get("hideMenu"))
- self.assertFalse(output["options"].get("hideLegend"))
- self.assertFalse(output["options"].get("hideOverview"))
- self.assertFalse(output["options"].get("hideNavbar"))
- self.assertEqual("dock", output["options"].get("popupLocation"))
- self.assertTrue(output["options"].get("fixed_scale_overview_map"))
-
- # Layers page
- self.assertIsNone(output["options"].get("hideGroupCheckbox"))
- self.assertIsNone(output["options"].get("activateFirstMapTheme"))
-
- # Baselayers page
- self.assertIsNone(output["options"].get("emptyBaselayer"))
- self.assertIsNone(output["options"].get("startupBaselayer"))
-
- # Attribute page
- self.assertIsNone(output["options"].get("limitDataToBbox"))
-
- # Layouts page
- self.assertFalse(output["options"].get("default_popup_print"))
-
- # Dataviz page
- self.assertIsNone(output["options"].get("datavizTemplate"))
- self.assertIsNone(output["options"].get("dataviz_drag_drop"))
- self.assertEqual("dock", output["options"].get("datavizLocation"))
- self.assertIsNone(output["options"].get("theme")) # default value "dark" is not set
-
- # Time manager page
- self.assertEqual(10, output["options"].get("tmTimeFrameSize"))
- self.assertEqual("seconds", output["options"].get("tmTimeFrameType"))
- self.assertEqual(1000, output["options"].get("tmAnimationFrameLength"))
-
- # Atlas page
- self.assertIsNone(output["options"].get("atlasShowAtStartup"))
- self.assertIsNone(output["options"].get("atlasAutoPlay"))
-
- def test_default_options_values_3_9(self, data: Path):
- """Test default options values."""
- lizmap = self._setup_empty_project(data)
-
- output = lizmap.project_config_file(LwcVersions.Lizmap_3_9, check_server=False, ignore_error=True)
-
- # generic options
- self.assertIsNone(output["options"].get("hideProject"))
- self.assertFalse(output["options"].get("automatic_permalink"))
- self.assertFalse(output["options"].get("wms_single_request_for_all_layers"))
- self.assertIsNone(output["options"].get("acl"))
-
- # map tools
- self.assertFalse(output["options"].get("measure"))
- self.assertIsNone(output["options"].get("print")) # The checkbox is removed since LWC 3.7.0
- self.assertIsNone(output["options"].get("zoomHistory")) # The checkbox is removed since LWC 3.8.0
- self.assertFalse(output["options"].get("geolocation"))
- # self.assertIsNone(output["options"].get("geolocationPrecision")) # Added since LWC 3.10.0
- # self.assertIsNone(output["options"].get("geolocationDirection")) # Added since LWC 3.10.0
- self.assertFalse(output["options"].get("draw"))
- self.assertIsNone(output["options"].get("externalSearch"))
- self.assertEqual(25, output["options"].get("pointTolerance"))
- self.assertEqual(10, output["options"].get("lineTolerance"))
- self.assertEqual(5, output["options"].get("polygonTolerance"))
-
- # API keys
- self.assertIsNone(output["options"].get("googleKey"))
- self.assertIsNone(output["options"].get("bingKey"))
- self.assertIsNone(output["options"].get("ignKey"))
-
- # Scales
- self.assertFalse(output["options"].get("use_native_zoom_levels"))
- self.assertFalse(output["options"].get("hide_numeric_scale_value"))
- self.assertEqual([10000, 25000, 50000, 100000, 250000, 500000], output["options"].get("mapScales"))
- self.assertEqual(1, output["options"].get("minScale"))
- self.assertEqual(1000000000, output["options"].get("maxScale"))
- self.assertIsNone(output["options"].get("max_scale_points"))
- self.assertIsNone(output["options"].get("max_scale_lines_polygons"))
-
- # Map interface
- self.assertFalse(output["options"].get("hideHeader"))
- self.assertFalse(output["options"].get("hideMenu"))
- self.assertFalse(output["options"].get("hideLegend"))
- self.assertFalse(output["options"].get("hideOverview"))
- self.assertFalse(output["options"].get("hideNavbar"))
- self.assertEqual("dock", output["options"].get("popupLocation"))
- self.assertTrue(output["options"].get("fixed_scale_overview_map"))
-
- # Layers page
- self.assertIsNone(output["options"].get("hideGroupCheckbox"))
- self.assertIsNone(output["options"].get("activateFirstMapTheme"))
-
- # Baselayers page
- self.assertIsNone(output["options"].get("emptyBaselayer"))
- self.assertIsNone(output["options"].get("startupBaselayer"))
-
- # Attribute page
- self.assertIsNone(output["options"].get("limitDataToBbox"))
-
- # Layouts page
- self.assertFalse(output["options"].get("default_popup_print"))
-
- # Dataviz page
- self.assertIsNone(output["options"].get("datavizTemplate"))
- self.assertIsNone(output["options"].get("dataviz_drag_drop"))
- self.assertEqual("dock", output["options"].get("datavizLocation"))
- self.assertIsNone(output["options"].get("theme")) # default value "dark" is not set
-
- # Time manager page
- self.assertEqual(10, output["options"].get("tmTimeFrameSize"))
- self.assertEqual("seconds", output["options"].get("tmTimeFrameType"))
- self.assertEqual(1000, output["options"].get("tmAnimationFrameLength"))
-
- # Atlas page
- self.assertIsNone(output["options"].get("atlasShowAtStartup"))
- self.assertIsNone(output["options"].get("atlasAutoPlay"))
-
- def test_default_options_latest(self, data: Path):
- """Test default options values."""
- lizmap = self._setup_empty_project(data)
-
- output = lizmap.project_config_file(LwcVersions.latest(), check_server=False, ignore_error=True)
-
- # generic options
- self.assertIsNone(output["options"].get("hideProject"))
- self.assertFalse(output["options"].get("automatic_permalink"))
- self.assertFalse(output["options"].get("wms_single_request_for_all_layers"))
- self.assertIsNone(output["options"].get("acl"))
-
- # map tools
- self.assertFalse(output["options"].get("measure"))
- self.assertIsNone(output["options"].get("print")) # The checkbox is removed since LWC 3.7.0
- self.assertIsNone(output["options"].get("zoomHistory")) # The checkbox is removed since LWC 3.8.0
- self.assertFalse(output["options"].get("geolocation"))
- self.assertTrue(output["options"].get("geolocationPrecision")) # Added since LWC 3.10.0
- self.assertFalse(output["options"].get("geolocationDirection")) # Added since LWC 3.10.0
- self.assertFalse(output["options"].get("draw"))
- self.assertIsNone(output["options"].get("externalSearch"))
- self.assertEqual(25, output["options"].get("pointTolerance"))
- self.assertEqual(10, output["options"].get("lineTolerance"))
- self.assertEqual(5, output["options"].get("polygonTolerance"))
-
- # API keys
- self.assertIsNone(output["options"].get("googleKey"))
- self.assertIsNone(output["options"].get("bingKey"))
- self.assertIsNone(output["options"].get("ignKey"))
-
- # Scales
- self.assertFalse(output["options"].get("use_native_zoom_levels"))
- self.assertFalse(output["options"].get("hide_numeric_scale_value"))
- self.assertEqual([10000, 25000, 50000, 100000, 250000, 500000], output["options"].get("mapScales"))
- self.assertEqual(1, output["options"].get("minScale"))
- self.assertEqual(1000000000, output["options"].get("maxScale"))
- self.assertIsNone(output["options"].get("max_scale_points"))
- self.assertIsNone(output["options"].get("max_scale_lines_polygons"))
-
- # Map interface
- self.assertFalse(output["options"].get("hideHeader"))
- self.assertFalse(output["options"].get("hideMenu"))
- self.assertFalse(output["options"].get("hideLegend"))
- self.assertFalse(output["options"].get("hideOverview"))
- self.assertFalse(output["options"].get("hideNavbar"))
- self.assertEqual("dock", output["options"].get("popupLocation"))
- self.assertTrue(output["options"].get("fixed_scale_overview_map"))
-
- # Layers page
- self.assertIsNone(output["options"].get("hideGroupCheckbox"))
- self.assertIsNone(output["options"].get("activateFirstMapTheme"))
-
- # Baselayers page
- self.assertIsNone(output["options"].get("emptyBaselayer"))
- self.assertIsNone(output["options"].get("startupBaselayer"))
-
- # Attribute page
- self.assertIsNone(output["options"].get("limitDataToBbox"))
-
- # Layouts page
- self.assertFalse(output["options"].get("default_popup_print"))
-
- # Dataviz page
- self.assertIsNone(output["options"].get("datavizTemplate"))
- self.assertIsNone(output["options"].get("dataviz_drag_drop"))
- self.assertEqual("dock", output["options"].get("datavizLocation"))
- self.assertIsNone(output["options"].get("theme")) # default value "dark" is not set
-
- # Time manager page
- self.assertEqual(10, output["options"].get("tmTimeFrameSize"))
- self.assertEqual("seconds", output["options"].get("tmTimeFrameType"))
- self.assertEqual(1000, output["options"].get("tmAnimationFrameLength"))
-
- # Atlas page
- self.assertIsNone(output["options"].get("atlasShowAtStartup"))
- self.assertIsNone(output["options"].get("atlasAutoPlay"))
-
- def test_max_scale_lwc_3_7(self, data: Path):
- """Test about maximum scale when zooming."""
- lizmap = self._setup_empty_project(data, LwcVersions.Lizmap_3_6)
-
- self.assertEqual(5000.0, lizmap.dlg.max_scale_points.scale())
- self.assertEqual(5000.0, lizmap.dlg.max_scale_lines_polygons.scale())
-
- # Max scale when zoomin
- # Only points with a different value
- lizmap.dlg.max_scale_points.setScale(1000.0)
-
- # Check new values in the output config
- output = lizmap.project_config_file(
- LwcVersions.latest(),
- check_server=False,
- ignore_error=True,
- )
-
- # Check scales in the CFG
- self.assertEqual(1000.0, output["options"]["max_scale_points"])
- self.assertIsNone(output["options"].get("max_scale_lines_polygons"))
-
- def test_general_scales_properties_lwc_3_6(self, data: Path):
- """Test some UI settings about general properties with LWC 3.6."""
- lizmap = self._setup_empty_project(data, LwcVersions.Lizmap_3_6)
-
- # Check default values
- self.assertEqual("10000, 25000, 50000, 100000, 250000, 500000", lizmap.dlg.list_map_scales.text())
-
- # Default values from config.py at the beginning only
- self.assertEqual("1", lizmap.dlg.minimum_scale.text())
- self.assertEqual("1000000000", lizmap.dlg.maximum_scale.text())
-
- # Trigger the signal
- lizmap.get_min_max_scales()
-
- # Values from the UI
- self.assertEqual("10000", lizmap.dlg.minimum_scale.text())
- self.assertEqual("500000", lizmap.dlg.maximum_scale.text())
-
- scales = "1000, 5000, 15000"
-
- # Fill scales
- lizmap.dlg.list_map_scales.setText(scales)
- lizmap.get_min_max_scales()
- self.assertEqual("1000", lizmap.dlg.minimum_scale.text())
- self.assertEqual("15000", lizmap.dlg.maximum_scale.text())
- self.assertEqual(scales, lizmap.dlg.list_map_scales.text())
-
- # Check new values in the output config
- output = lizmap.project_config_file(LwcVersions.latest(), check_server=False, ignore_error=True)
-
- # Check scales in the CFG
- self.assertEqual(1000, output["options"]["minScale"])
- self.assertEqual(15000, output["options"]["maxScale"])
- self.assertListEqual([1000, 5000, 15000], output["options"]["mapScales"])
-
- # Project is in EPSG:2154, must be False
- self.assertFalse(output["options"]["use_native_zoom_levels"])
-
- # Check an empty list and a populated list then
- self.assertIsNone(output["options"].get("acl"))
- lizmap.dlg.inAcl.setText("cadastre,urbanism")
-
- output = lizmap.project_config_file(
- LwcVersions.latest(),
- check_server=False,
- ignore_error=True,
- )
- self.assertListEqual(["cadastre", "urbanism"], output["options"].get("acl"))
-
- def test_read_existing_lwc_3_6_to_3_7(self, data: Path):
- """Test to read a CFG 3.6 and to export it to 3.7 about scales."""
- # Checking CFG before opening the QGS file
- with data.joinpath("3857_project_lwc_3_6.qgs.cfg").open() as f:
- json_data = json.load(f)
- self.assertListEqual([1000, 5000, 10000, 500000], json_data["options"]["mapScales"])
-
- project = QgsProject.instance()
- project.read(str(data.joinpath("3857_project_lwc_3_6.qgs")))
- self.assertEqual(1, len(project.mapLayers()))
-
- lizmap = Lizmap(get_iface(), lwc_version=LwcVersions.Lizmap_3_7)
- # read_cfg_file will call "layers_config_file"
- lizmap.read_cfg_file(skip_tables=True)
-
- self.assertEqual("1000", lizmap.dlg.minimum_scale.text())
- self.assertEqual("500000", lizmap.dlg.maximum_scale.text())
- self.assertEqual("1000, 5000, 10000, 500000", lizmap.dlg.list_map_scales.text())
-
- output = lizmap.project_config_file(
- LwcVersions.Lizmap_3_7,
- check_server=False,
- ignore_error=True,
- )
-
- # Project is in EPSG:3857, must be True
- self.assertTrue(output["options"]["use_native_zoom_levels"])
- # only two when we save
- self.assertListEqual([1000, 500000], output["options"]["mapScales"])
- self.assertEqual(1000, output["options"]["minScale"])
- self.assertEqual(500000, output["options"]["maxScale"])
-
- def test_read_existing_lwc_3_6_to_3_6(self, data: Path):
- """Test to read a CFG 3.6 and to export it to 3.6 about scales."""
- # Checking CFG before opening the QGS file
- project = QgsProject.instance()
- project.read(str(data.joinpath("3857_project_lwc_3_6.qgs")))
- self.assertEqual(1, len(project.mapLayers()))
-
- lizmap = Lizmap(get_iface(), lwc_version=LwcVersions.Lizmap_3_6)
- # read_cfg_file will call "layers_config_file"
- lizmap.read_cfg_file(skip_tables=True)
-
- self.assertEqual("1000", lizmap.dlg.minimum_scale.text())
- self.assertEqual("500000", lizmap.dlg.maximum_scale.text())
- self.assertEqual("1000, 5000, 10000, 500000", lizmap.dlg.list_map_scales.text())
-
- output = lizmap.project_config_file(
- LwcVersions.Lizmap_3_6,
- check_server=False,
- ignore_error=True,
- )
-
- # Project is in EPSG:3857, must be False because of LWC 3.6
- self.assertFalse(output["options"]["use_native_zoom_levels"])
-
- self.assertListEqual([1000, 5000, 10000, 500000], output["options"]["mapScales"])
- self.assertEqual(1000, output["options"]["minScale"])
- self.assertEqual(500000, output["options"]["maxScale"])
-
- def test_atlas_auto_play_true_values(self, data: Path):
- """Test some UI settings about boolean values."""
- lizmap = self._setup_empty_project(data)
-
- output = lizmap.project_config_file(LwcVersions.latest(), check_server=False, ignore_error=True)
- self.assertIsNone(output["options"].get("atlasAutoPlay"))
-
- lizmap.dlg.atlasAutoPlay.setChecked(True)
-
- output = lizmap.project_config_file(LwcVersions.latest(), check_server=False, ignore_error=True)
- self.assertTrue(output["options"].get("atlasAutoPlay"))
-
- lizmap.dlg.atlasAutoPlay.setChecked(False)
-
- output = lizmap.project_config_file(
- LwcVersions.latest(),
- check_server=False,
- ignore_error=True,
- )
-
- self.assertIsNone(output["options"].get("atlasAutoPlay"))
-
- def test_geolocation_values(self, data: Path):
- """Test geolocation UI settings."""
- lizmap = self._setup_empty_project(data)
-
- # Default geolocation checkboxes checked
- self.assertFalse(lizmap.dlg.groupbox_geolocation.isChecked())
- self.assertTrue(lizmap.dlg.checkbox_geolocation_precision.isChecked())
- self.assertFalse(lizmap.dlg.checkbox_geolocation_direction.isChecked())
-
- # Default geolocation checkboxes enabled
- self.assertTrue(lizmap.dlg.groupbox_geolocation.isEnabled())
- self.assertFalse(lizmap.dlg.checkbox_geolocation_precision.isEnabled())
- self.assertFalse(lizmap.dlg.checkbox_geolocation_direction.isEnabled())
-
- # Check geolocation checkbox to enable other checkboxes
- lizmap.dlg.groupbox_geolocation.setChecked(True)
- self.assertTrue(lizmap.dlg.checkbox_geolocation_precision.isEnabled())
- self.assertTrue(lizmap.dlg.checkbox_geolocation_direction.isEnabled())
-
- output = lizmap.project_config_file(LwcVersions.latest(), check_server=False, ignore_error=True)
- # options
- self.assertTrue(output["options"].get("geolocation"))
- self.assertTrue(output["options"].get("geolocationPrecision"))
- self.assertFalse(output["options"].get("geolocationDirection"))
-
- # Check direction
- lizmap.dlg.checkbox_geolocation_direction.setChecked(True)
- # Uncheck precision
- lizmap.dlg.checkbox_geolocation_precision.setChecked(False)
-
- output = lizmap.project_config_file(LwcVersions.latest(), check_server=False, ignore_error=True)
- # options
- self.assertTrue(output["options"].get("geolocation"))
- self.assertFalse(output["options"].get("geolocationPrecision"))
- self.assertTrue(output["options"].get("geolocationDirection"))
-
- def test_exclude_basemaps_from_single_wms_values(self, data: Path):
- """Test exclude basemaps from single WMS UI settings."""
- lizmap = self._setup_empty_project(data)
-
- # Default checkbox states
- self.assertFalse(lizmap.dlg.checkbox_wms_single_request_all_layers.isChecked())
- self.assertFalse(lizmap.dlg.checkbox_exclude_basemaps_from_single_wms.isChecked())
-
- # Exclude basemaps checkbox should be disabled when single WMS is off
- self.assertFalse(lizmap.dlg.checkbox_exclude_basemaps_from_single_wms.isEnabled())
-
- # Enable single WMS - exclude basemaps checkbox should become enabled
- lizmap.dlg.checkbox_wms_single_request_all_layers.setChecked(True)
- self.assertTrue(lizmap.dlg.checkbox_exclude_basemaps_from_single_wms.isEnabled())
-
- output = lizmap.project_config_file(LwcVersions.latest(), check_server=False, ignore_error=True)
- # Single WMS enabled, exclude basemaps still unchecked
- self.assertTrue(output["options"].get("wms_single_request_for_all_layers"))
- self.assertFalse(output["options"].get("exclude_basemaps_from_single_wms"))
-
- # Enable exclude basemaps
- lizmap.dlg.checkbox_exclude_basemaps_from_single_wms.setChecked(True)
-
- output = lizmap.project_config_file(LwcVersions.latest(), check_server=False, ignore_error=True)
- # Both options enabled
- self.assertTrue(output["options"].get("wms_single_request_for_all_layers"))
- self.assertTrue(output["options"].get("exclude_basemaps_from_single_wms"))
-
- # Disable single WMS - exclude basemaps checkbox should become disabled
- lizmap.dlg.checkbox_wms_single_request_all_layers.setChecked(False)
- self.assertFalse(lizmap.dlg.checkbox_exclude_basemaps_from_single_wms.isEnabled())
-
- def test_group_popup_by_layer(self, data: Path):
- """Test group popup by layer UI settings."""
- lizmap = self._setup_empty_project(data)
-
- # Default checkbox states
- self.assertFalse(lizmap.dlg.checkbox_group_popup_by_layer.isChecked())
-
- # Enable group popup by layer
- lizmap.dlg.checkbox_group_popup_by_layer.setChecked(True)
- self.assertTrue(lizmap.dlg.checkbox_group_popup_by_layer.isEnabled())
-
- output = lizmap.project_config_file(LwcVersions.latest(), check_server=False, ignore_error=True)
- # Group popup by layers option should be checked in output config file
- self.assertTrue(output["options"].get("group_popup_by_layer"))
-
- # Disable group popup by layer
- lizmap.dlg.checkbox_group_popup_by_layer.setChecked(False)
- output = lizmap.project_config_file(LwcVersions.latest(), check_server=False, ignore_error=True)
- # Group popup by layers option should be unchecked in output config file
- self.assertFalse(output["options"].get("group_popup_by_layer"))
-
- def test_short_link_permalink(self, data: Path):
- """Test short link permalink UI settings."""
- lizmap = self._setup_empty_project(data)
-
- # Default checkbox states
- self.assertFalse(lizmap.dlg.checkbox_short_link_permalink.isChecked())
-
- # Enable short link permalink
- lizmap.dlg.checkbox_short_link_permalink.setChecked(True)
- self.assertTrue(lizmap.dlg.checkbox_short_link_permalink.isEnabled())
-
- output = lizmap.project_config_file(LwcVersions.latest(), check_server=False, ignore_error=True)
- # Short link permalink option should be checked in output config file
- self.assertTrue(output["options"].get("short_link_permalink"))
-
- # Disable short link permalink
- lizmap.dlg.checkbox_short_link_permalink.setChecked(False)
- output = lizmap.project_config_file(LwcVersions.latest(), check_server=False, ignore_error=True)
- # Short link permalink option should be unchecked in output config file
- self.assertFalse(output["options"].get("short_link_permalink"))
+def test_ui_base(data: Path, qgis_iface: QgisInterface):
+ """Test opening the Lizmap dialog with some basic checks."""
+ project = QgsProject.instance()
+ project.clear()
+ lizmap = Lizmap(qgis_iface, lwc_version=LwcVersions.latest())
+
+ layer = QgsVectorLayer(str(data.joinpath("lines.geojson")), "lines", "ogr")
+ project.addMapLayer(layer)
+
+ layer = QgsVectorLayer(str(data.joinpath("points.geojson")), "points", "ogr")
+ project.addMapLayer(layer)
+
+ flag, message = lizmap.check_global_project_options()
+ assert not flag, message
+ assert message, (
+ "You need to open a QGIS project, using the QGS extension.
This is "
+ "needed before using other tabs in ""the plugin."
+ )
+
+ project.write(str(data.joinpath("unittest.qgs")))
+ flag, message = lizmap.check_global_project_options()
+ assert flag, message
+
+ # lizmap.run()
+ # lizmap.get_map_options()
+
+def test_legend_options(data: Path, qgis_iface: QgisInterface):
+ """Test about reading legend options."""
+ project = QgsProject.instance()
+ project.read(str(data.joinpath("legend_image_option.qgs")))
+ assert len(project.mapLayers()) == 3
+
+ lizmap = Lizmap(qgis_iface, lwc_version=LwcVersions.latest())
+ # read_cfg_file will call "layers_config_file"
+ config = lizmap.read_cfg_file(skip_tables=True)
+ print("\n::test_legend_options::config", config)
+
+ lizmap.process_node(lizmap.layerList, project.layerTreeRoot(), None, config)
+
+ assert lizmap.dlg.minimum_scale.text() == "5000"
+ assert lizmap.dlg.maximum_scale.text() == "500000"
+ assert lizmap.dlg.list_map_scales.text() == "5000, 250000, 500000"
+
+ assert lizmap.layerList.get("legend_disabled_layer_id").get("legend_image_option") == "disabled"
+ assert lizmap.layerList["legend_displayed_startup_layer_id"]["legend_image_option"] == "expand_at_startup"
+ assert lizmap.layerList["legend_hidden_startup_layer_id"]["legend_image_option"] == "hide_at_startup"
+
+ # For LWC 3.6
+ output = lizmap.project_config_file(
+ LwcVersions.Lizmap_3_6,
+ check_server=False,
+ ignore_error=True,
+ )
+
+ # NOTE: Seems that HTML widget not working in tests
+ # See lizmap.widgets.html_editor line 117
+ # assert '' in output['options']['datavizTemplate']
+
+ assert output["layers"]["legend_displayed_startup"]["legend_image_option"] == "expand_at_startup"
+ assert output["layers"]["legend_displayed_startup"].get("noLegendImage") is None
+ assert output["options"].get("default_background_color_index") is None
+
+ # For LWC 3.5
+ output = lizmap.project_config_file(
+ LwcVersions.Lizmap_3_5, with_gui=False, check_server=False, ignore_error=True
+ )
+ assert output["layers"]["legend_displayed_startup"].get("legend_image_option") is None
+ assert str(False) == output["layers"]["legend_displayed_startup"]["noLegendImage"]
+
+def _setup_empty_project(
+ data: Path,
+ qgis_iface: QgisInterface,
+ lwc_version: LwcVersions = LwcVersions.latest(),
+) -> Lizmap:
+ """Internal function to add a layer and a basic check."""
+ project = QgsProject.instance()
+ layer = QgsVectorLayer(str(data.joinpath("lines.geojson")), "lines", "ogr")
+ project.addMapLayer(layer)
+ project.setFileName(temporary_file_path())
+
+ lizmap = Lizmap(qgis_iface, lwc_version=lwc_version)
+ baselayers = lizmap._add_group_legend("baselayers", exclusive=True, parent=None, project=project)
+ lizmap._add_group_legend(
+ "project-background-color", exclusive=False, parent=baselayers, project=project
+ )
+ hidden = lizmap._add_group_legend("hidden", project=project)
+
+ # For testing, we add OSM as hidden layer
+ hidden_raster = QgsRasterLayer(
+ "type=xyz&url=https://tile.openstreetmap.org/{z}/{x}/{y}.png", "OSM", "wms"
+ )
+ project.addMapLayer(hidden_raster, False)
+ hidden.addLayer(hidden_raster)
+
+ # Do not use read_lizmap_config_file
+ # as it will be called by read_cfg_file and also the UI is set in read_cfg_file
+ config = lizmap.read_cfg_file(skip_tables=True)
+
+ lizmap.dlg.widget_initial_extent.setOutputExtentFromLayer(layer)
+
+ # Config is empty in the CFG file because it's a new project
+ TestCase.assertDictEqual({}, config)
+
+ # Some process
+ lizmap.process_node(lizmap.layerList, project.layerTreeRoot(), None, {})
+
+ return lizmap
+
+def test_lizmap_layer_properties(data: Path, qgis_iface: QgisInterface):
+ """Test apply some properties in a layer in the dialog."""
+ lizmap = _setup_empty_project(data, qgis_iface)
+
+ # Click the layer
+ item = lizmap.dlg.layer_tree.topLevelItem(0)
+ assert item.text(0) == "lines"
+ assert item.text(1).startswith("lines_")
+ assert item.text(2) == "layer"
+ assert item.data(0, Qt.ItemDataRole.UserRole + 1) == PredefinedGroup.No.value
+ assert item.text(3) == "" # Not used, just to test
+
+ assert not lizmap.dlg.list_group_visibility.isEnabled(), "Visibility should be disabled"
+
+ # Click the first line
+ lizmap.dlg.layer_tree.setCurrentItem(lizmap.dlg.layer_tree.topLevelItem(0))
+
+ # Fill the ACL field
+ assert lizmap.dlg.list_group_visibility.isEnabled(), "Visibility should be enabled"
+ acl_layer = "a_group_id"
+ lizmap.dlg.list_group_visibility.setText(acl_layer)
+ lizmap.save_value_layer_group_data("group_visibility")
+
+ # Fill the abstract field
+ html_abstract = "Hello"
+ lizmap.dlg.teLayerAbstract.setPlainText(html_abstract)
+ lizmap.save_value_layer_group_data("abstract")
+
+ # Click the group base-layers
+ group_item = lizmap.dlg.layer_tree.findItems(
+ "baselayers", Qt.MatchFlag.MatchContains | Qt.MatchFlag.MatchRecursive, 0
+ )[0]
+ lizmap.dlg.layer_tree.setCurrentItem(group_item)
+ assert not lizmap.dlg.panel_layer_all_settings.isEnabled()
+
+ # Click the group project-background-color
+ group_item = lizmap.dlg.layer_tree.findItems(
+ "project-background-color", Qt.MatchFlag.MatchContains | Qt.MatchFlag.MatchRecursive, 0
+ )[0]
+ lizmap.dlg.layer_tree.setCurrentItem(group_item)
+ assert lizmap.dlg.panel_layer_all_settings.isEnabled()
+ assert lizmap.dlg.group_layer_metadata.isEnabled()
+ # XXX QGIS4 KO
+ assert not lizmap.dlg.group_layer_tree_options.isEnabled(), "Layer tree option should be disabled"
+
+ # Click the group hidden
+ group_item = lizmap.dlg.layer_tree.findItems(
+ "hidden", Qt.MatchFlag.MatchContains | Qt.MatchFlag.MatchRecursive, 0
+ )[0]
+ lizmap.dlg.layer_tree.setCurrentItem(group_item)
+ # It should work, maybe the test click and click in the UI is missing one thing
+ # assert lizmap._current_item_predefined_group() == PredefinedGroup.Hidden.value
+ # assert not lizmap.dlg.panel_layer_all_settings.isEnabled()
+
+ # Back to a layer outside of these groups
+ group_item = lizmap.dlg.layer_tree.findItems(
+ "lines", Qt.MatchFlag.MatchContains | Qt.MatchFlag.MatchRecursive, 0
+ )[0]
+ lizmap.dlg.layer_tree.setCurrentItem(group_item)
+ assert lizmap.dlg.list_group_visibility.isEnabled()
+
+ # Check new values in the output config
+ output = lizmap.project_config_file(
+ LwcVersions.latest(),
+ check_server=False,
+ ignore_error=True,
+ )
+
+ # Layers
+ assert output["layers"]["lines"]["group_visibility"] == [acl_layer]
+ assert output["layers"]["lines"]["abstract"] == html_abstract
+ # Predefined groups, still in the CFG
+ assert output["layers"]["baselayers"]["group_visibility"] == []
+ assert output["layers"]["baselayers"]["abstract"] == ""
+ assert output["layers"]["project-background-color"]["group_visibility"] == []
+ assert output["layers"]["project-background-color"]["abstract"] == ""
+ # XXX QGIS4 KO
+ assert output["options"]["default_background_color_index"] == 0
+
+ assert not output["layers"]["lines"].get("children_lizmap_features_table")
+ assert output["layers"]["lines"].get("popupDisplayChildren") == "False"
+
+ # Test a false value as a string which shouldn't be there by default
+ assert output["layers"]["lines"].get("externalWmsToggle") is None
+ assert output["layers"]["lines"].get("metatileSize") is None
+
+def test_default_options_values_3_6(data: Path, qgis_iface: QgisInterface):
+ """Test default options values."""
+ lizmap = _setup_empty_project(data, qgis_iface)
+
+ output = lizmap.project_config_file(LwcVersions.Lizmap_3_6, check_server=False, ignore_error=True)
+
+ # generic options
+ assert output["options"].get("hideProject") is None
+ assert not output["options"].get("automatic_permalink")
+ assert not output["options"].get("wms_single_request_for_all_layers")
+ assert output["options"].get("acl") is None
+
+ # map tools
+ assert not output["options"].get("measure")
+ assert not output["options"].get("print") # The checkbox is removed since LWC 3.7.0
+ assert not output["options"].get("zoomHistory") # The checkbox is removed since LWC 3.8.0
+ assert not output["options"].get("geolocation")
+ assert not output["options"].get("draw")
+ assert output["options"].get("externalSearch") is None
+ assert output["options"]["pointTolerance"] == 25
+ assert output["options"]["lineTolerance"] == 10
+ assert output["options"]["polygonTolerance"] == 5
+
+ # API keys
+ assert output["options"].get("googleKey") is None
+ assert output["options"].get("bingKey") is None
+ assert output["options"].get("ignKey") is None
+
+ # Scales
+ assert not output["options"].get("use_native_zoom_levels")
+ assert not output["options"].get("hide_numeric_scale_value")
+ assert output["options"]["mapScales"] == [10000, 25000, 50000, 100000, 250000, 500000]
+ assert output["options"]["minScale"] == 1
+ assert output["options"]["maxScale"] == 1000000000
+ assert output["options"].get("max_scale_points") is None
+ assert output["options"].get("max_scale_lines_polygons") is None
+
+ # Map interface
+ assert not output["options"].get("hideHeader")
+ assert not output["options"].get("hideMenu")
+ assert not output["options"].get("hideLegend")
+ assert not output["options"].get("hideOverview")
+ assert not output["options"].get("hideNavbar")
+ assert output["options"]["popupLocation"] == "dock"
+ assert output["options"]["fixed_scale_overview_map"]
+
+ # Layers page
+ assert output["options"].get("hideGroupCheckbox") is None
+ assert output["options"].get("activateFirstMapTheme") is None
+
+ # Baselayers page
+ assert output["options"].get("emptyBaselayer") is None
+ assert output["options"].get("startupBaselayer") is None
+
+ # Attribute page
+ assert output["options"].get("limitDataToBbox") is None
+
+ # Layouts page
+ assert not output["options"].get("default_popup_print")
+
+ # Dataviz page
+ assert output["options"].get("datavizTemplate") is None
+ assert output["options"].get("dataviz_drag_drop") is None
+ assert output["options"]["datavizLocation"] == "dock"
+ assert output["options"].get("theme") is None # default value "dark" is not set
+
+ # Time manager page
+ assert output["options"]["tmTimeFrameSize"] == 10
+ assert output["options"]["tmTimeFrameType"] == "seconds"
+ assert output["options"]["tmAnimationFrameLength"] == 1000
+
+ # Atlas page
+ assert output["options"].get("atlasShowAtStartup") is None
+ assert output["options"].get("atlasAutoPlay") is None
+
+def test_default_options_values_3_7(data: Path, qgis_iface: QgisInterface):
+ """Test default options values."""
+ lizmap = _setup_empty_project(data, qgis_iface)
+
+ output = lizmap.project_config_file(LwcVersions.Lizmap_3_7, check_server=False, ignore_error=True)
+
+ # generic options
+ output["options"].get("hideProject") is None
+ assert not output["options"].get("automatic_permalink")
+ assert not output["options"].get("wms_single_request_for_all_layers")
+ assert output["options"].get("acl") is None
+
+ # map tools
+ assert not output["options"].get("measure")
+ assert output["options"].get("print") is None # The checkbox is removed since LWC 3.7.0
+ assert not output["options"].get("zoomHistory") # The checkbox is removed since LWC 3.8.0
+ assert not output["options"].get("geolocation")
+ assert not output["options"].get("draw")
+ assert output["options"].get("externalSearch") is None
+ assert output["options"]["pointTolerance"] == 25
+ assert output["options"]["lineTolerance"] == 10
+ assert output["options"]["polygonTolerance"] == 5
+
+ # API keys
+ assert output["options"].get("googleKey") is None
+ assert output["options"].get("bingKey") is None
+ assert output["options"].get("ignKey") is None
+
+ # Scales
+ assert not output["options"].get("use_native_zoom_levels")
+ assert not output["options"].get("hide_numeric_scale_value")
+ assert output["options"]["mapScales"] == [10000, 25000, 50000, 100000, 250000, 500000]
+ assert output["options"]["minScale"] == 1
+ assert output["options"]["maxScale"] == 1000000000
+ assert output["options"].get("max_scale_points") is None
+ assert output["options"].get("max_scale_lines_polygons") is None
+
+ # Map interface
+ assert not output["options"].get("hideHeader")
+ assert not output["options"].get("hideMenu")
+ assert not output["options"].get("hideLegend")
+ assert not output["options"].get("hideOverview")
+ assert not output["options"].get("hideNavbar")
+ assert output["options"]["popupLocation"] == "dock"
+ assert output["options"].get("fixed_scale_overview_map")
+
+ # Layers page
+ assert output["options"].get("hideGroupCheckbox") is None
+ assert output["options"].get("activateFirstMapTheme") is None
+
+ # Baselayers page
+ assert output["options"].get("emptyBaselayer") is None
+ assert output["options"].get("startupBaselayer") is None
+
+ # Attribute page
+ assert output["options"].get("limitDataToBbox") is None
+
+ # Layouts page
+ assert not output["options"].get("default_popup_print")
+
+ # Dataviz page
+ assert output["options"].get("datavizTemplate") is None
+ assert output["options"].get("dataviz_drag_drop") is None
+ assert output["options"]["datavizLocation"] == "dock"
+ assert output["options"].get("theme") is None # default value "dark" is not set
+
+ # Time manager page
+ assert output["options"]["tmTimeFrameSize"] == 10
+ assert output["options"]["tmTimeFrameType"] == "seconds"
+ assert output["options"]["tmAnimationFrameLength"] == 1000
+
+ # Atlas page
+ assert output["options"].get("atlasShowAtStartup") is None
+ assert output["options"].get("atlasAutoPlay") is None
+
+def test_default_options_values_3_8(data: Path, qgis_iface: QgisInterface):
+ """Test default options values."""
+ lizmap = _setup_empty_project(data, qgis_iface)
+
+ output = lizmap.project_config_file(LwcVersions.Lizmap_3_8, check_server=False, ignore_error=True)
+
+ # generic options
+ assert output["options"].get("hideProject") is None
+ assert not output["options"].get("automatic_permalink")
+ assert not output["options"].get("wms_single_request_for_all_layers")
+ assert output["options"].get("acl") is None
+
+ # map tools
+ assert not output["options"].get("measure")
+ assert output["options"].get("print") is None # The checkbox is removed since LWC 3.7.0
+ assert output["options"].get("zoomHistory") is None # The checkbox is removed since LWC 3.8.0
+ assert not output["options"].get("geolocation")
+ assert not output["options"].get("draw")
+ assert output["options"].get("externalSearch") is None
+ assert output["options"].get("pointTolerance") == 25
+ assert output["options"].get("lineTolerance") == 10
+ assert output["options"].get("polygonTolerance") == 5
+
+ # API keys
+ assert output["options"].get("googleKey") is None
+ assert output["options"].get("bingKey") is None
+ assert output["options"].get("ignKey") is None
+
+ # Scales
+ assert not output["options"].get("use_native_zoom_levels")
+ assert not output["options"].get("hide_numeric_scale_value")
+ assert output["options"].get("mapScales") == [10000, 25000, 50000, 100000, 250000, 500000]
+ assert output["options"].get("minScale") == 1
+ assert output["options"].get("maxScale") == 1000000000
+ assert output["options"].get("max_scale_points") is None
+ assert output["options"].get("max_scale_lines_polygons") is None
+
+ # Map interface
+ assert not output["options"].get("hideHeader")
+ assert not output["options"].get("hideMenu")
+ assert not output["options"].get("hideLegend")
+ assert not output["options"].get("hideOverview")
+ assert not output["options"].get("hideNavbar")
+ assert output["options"].get("popupLocation") == "dock"
+ assert output["options"].get("fixed_scale_overview_map")
+
+ # Layers page
+ assert output["options"].get("hideGroupCheckbox") is None
+ assert output["options"].get("activateFirstMapTheme") is None
+
+ # Baselayers page
+ assert output["options"].get("emptyBaselayer") is None
+ assert output["options"].get("startupBaselayer") is None
+
+ # Attribute page
+ assert output["options"].get("limitDataToBbox") is None
+
+ # Layouts page
+ assert not output["options"].get("default_popup_print")
+
+ # Dataviz page
+ assert output["options"].get("datavizTemplate") is None
+ assert output["options"].get("dataviz_drag_drop") is None
+ assert output["options"].get("datavizLocation") == "dock"
+ assert output["options"].get("theme") is None # default value "dark" is not set
+
+ # Time manager page
+ assert output["options"].get("tmTimeFrameSize") == 10
+ assert output["options"].get("tmTimeFrameType") == "seconds"
+ assert output["options"].get("tmAnimationFrameLength") == 1000
+
+ # Atlas page
+ assert output["options"].get("atlasShowAtStartup") is None
+ assert output["options"].get("atlasAutoPlay") is None
+
+def test_default_options_values_3_9(data: Path, qgis_iface: QgisInterface):
+ """Test default options values."""
+ lizmap = _setup_empty_project(data, qgis_iface)
+
+ output = lizmap.project_config_file(LwcVersions.Lizmap_3_9, check_server=False, ignore_error=True)
+
+ # generic options
+ assert output["options"].get("hideProject") is None
+ assert not output["options"].get("automatic_permalink")
+ assert not output["options"].get("wms_single_request_for_all_layers")
+ assert output["options"].get("acl") is None
+
+ # map tools
+ assert not output["options"].get("measure")
+ assert output["options"].get("print") is None # The checkbox is removed since LWC 3.7.0
+ assert output["options"].get("zoomHistory") is None # The checkbox is removed since LWC 3.8.0
+ assert not output["options"].get("geolocation")
+ # assert output["options"].get("geolocationPrecision") is None # Added since LWC 3.10.0
+ # assert output["options"].get("geolocationDirection") is None # Added since LWC 3.10.0
+ assert not output["options"].get("draw")
+ assert output["options"].get("externalSearch") is None
+ assert output["options"].get("pointTolerance") == 25
+ assert output["options"].get("lineTolerance") == 10
+ assert output["options"].get("polygonTolerance") == 5
+
+ # API keys
+ assert output["options"].get("googleKey") is None
+ assert output["options"].get("bingKey") is None
+ assert output["options"].get("ignKey") is None
+
+ # Scales
+ assert not output["options"].get("use_native_zoom_levels")
+ assert not output["options"].get("hide_numeric_scale_value")
+ assert output["options"].get("mapScales") == [10000, 25000, 50000, 100000, 250000, 500000]
+ assert output["options"].get("minScale") == 1
+ assert output["options"].get("maxScale") == 1000000000
+ assert output["options"].get("max_scale_points") is None
+ assert output["options"].get("max_scale_lines_polygons") is None
+
+ # Map interface
+ assert not output["options"].get("hideHeader")
+ assert not output["options"].get("hideMenu")
+ assert not output["options"].get("hideLegend")
+ assert not output["options"].get("hideOverview")
+ assert not output["options"].get("hideNavbar")
+ assert output["options"].get("popupLocation") == "dock"
+ assert output["options"].get("fixed_scale_overview_map")
+
+ # Layers page
+ assert output["options"].get("hideGroupCheckbox") is None
+ assert output["options"].get("activateFirstMapTheme") is None
+
+ # Baselayers page
+ assert output["options"].get("emptyBaselayer") is None
+ assert output["options"].get("startupBaselayer") is None
+
+ # Attribute page
+ assert output["options"].get("limitDataToBbox") is None
+
+ # Layouts page
+ assert not output["options"].get("default_popup_print")
+
+ # Dataviz page
+ assert output["options"].get("datavizTemplate") is None
+ assert output["options"].get("dataviz_drag_drop") is None
+ assert output["options"].get("datavizLocation") == "dock"
+ assert output["options"].get("theme") is None # default value "dark" is not set
+
+ # Time manager page
+ assert output["options"].get("tmTimeFrameSize") == 10
+ assert output["options"].get("tmTimeFrameType") == "seconds"
+ assert output["options"].get("tmAnimationFrameLength") == 1000
+
+ # Atlas page
+ assert output["options"].get("atlasShowAtStartup") is None
+ assert output["options"].get("atlasAutoPlay") is None
+
+def test_default_options_latest(data: Path, qgis_iface: QgisInterface):
+ """Test default options values."""
+ lizmap = _setup_empty_project(data, qgis_iface)
+
+ output = lizmap.project_config_file(LwcVersions.latest(), check_server=False, ignore_error=True)
+
+ # generic options
+ assert output["options"].get("hideProject") is None
+ assert not output["options"].get("automatic_permalink")
+ assert not output["options"].get("wms_single_request_for_all_layers")
+ assert output["options"].get("acl") is None
+
+ # map tools
+ assert not output["options"].get("measure")
+ assert output["options"].get("print") is None # The checkbox is removed since LWC 3.7.0
+ assert output["options"].get("zoomHistory") is None # The checkbox is removed since LWC 3.8.0
+ assert not output["options"].get("geolocation")
+ assert output["options"].get("geolocationPrecision") # Added since LWC 3.10.0
+ assert not output["options"].get("geolocationDirection") # Added since LWC 3.10.0
+ assert not output["options"].get("draw")
+ assert output["options"].get("externalSearch") is None
+ assert output["options"].get("pointTolerance") == 25
+ assert output["options"].get("lineTolerance") == 10
+ assert output["options"].get("polygonTolerance") == 5
+
+ # API keys
+ assert output["options"].get("googleKey") is None
+ assert output["options"].get("bingKey") is None
+ assert output["options"].get("ignKey") is None
+
+ # Scales
+ assert not output["options"].get("use_native_zoom_levels")
+ assert not output["options"].get("hide_numeric_scale_value")
+ assert output["options"].get("mapScales") == [10000, 25000, 50000, 100000, 250000, 500000]
+ assert output["options"].get("minScale") == 1
+ assert output["options"].get("maxScale") == 1000000000
+ assert output["options"].get("max_scale_points") is None
+ assert output["options"].get("max_scale_lines_polygons") is None
+
+ # Map interface
+ assert not output["options"].get("hideHeader")
+ assert not output["options"].get("hideMenu")
+ assert not output["options"].get("hideLegend")
+ assert not output["options"].get("hideOverview")
+ assert not output["options"].get("hideNavbar")
+ assert output["options"].get("popupLocation") == "dock"
+ assert output["options"].get("fixed_scale_overview_map")
+
+ # Layers page
+ assert output["options"].get("hideGroupCheckbox") is None
+ assert output["options"].get("activateFirstMapTheme") is None
+
+ # Baselayers page
+ assert output["options"].get("emptyBaselayer") is None
+ assert output["options"].get("startupBaselayer") is None
+
+ # Attribute page
+ assert output["options"].get("limitDataToBbox") is None
+
+ # Layouts page
+ assert not output["options"].get("default_popup_print")
+
+ # Dataviz page
+ assert output["options"].get("datavizTemplate") is None
+ assert output["options"].get("dataviz_drag_drop") is None
+ assert output["options"].get("datavizLocation") == "dock"
+ assert output["options"].get("theme") is None # default value "dark" is not set
+
+ # Time manager page
+ assert output["options"].get("tmTimeFrameSize") == 10
+ assert output["options"].get("tmTimeFrameType") == "seconds"
+ assert output["options"].get("tmAnimationFrameLength") == 1000
+
+ # Atlas page
+ assert output["options"].get("atlasShowAtStartup") is None
+ assert output["options"].get("atlasAutoPlay") is None
+
+def test_max_scale_lwc_3_7(data: Path, qgis_iface: QgisInterface):
+ """Test about maximum scale when zooming."""
+ lizmap = _setup_empty_project(data, qgis_iface, LwcVersions.Lizmap_3_6)
+
+ assert lizmap.dlg.max_scale_points.scale() == 5000.0
+ assert lizmap.dlg.max_scale_lines_polygons.scale() == 5000.0
+
+ # Max scale when zoomin
+ # Only points with a different value
+ lizmap.dlg.max_scale_points.setScale(1000.0)
+
+ # Check new values in the output config
+ output = lizmap.project_config_file(
+ LwcVersions.latest(),
+ check_server=False,
+ ignore_error=True,
+ )
+
+ # Check scales in the CFG
+ assert output["options"]["max_scale_points"] == 1000.0
+ assert output["options"].get("max_scale_lines_polygons") is None
+
+def test_general_scales_properties_lwc_3_6(data: Path, qgis_iface: QgisInterface):
+ """Test some UI settings about general properties with LWC 3.6."""
+ lizmap = _setup_empty_project(data, qgis_iface, LwcVersions.Lizmap_3_6)
+
+ # Check default values
+ assert lizmap.dlg.list_map_scales.text() == "10000, 25000, 50000, 100000, 250000, 500000"
+
+ # Default values from config.py at the beginning only
+ assert lizmap.dlg.minimum_scale.text() == "1"
+ assert lizmap.dlg.maximum_scale.text() == "1000000000"
+
+ # Trigger the signal
+ lizmap.get_min_max_scales()
+
+ # Values from the UI
+ assert lizmap.dlg.minimum_scale.text() == "10000"
+ assert lizmap.dlg.maximum_scale.text() == "500000"
+
+ scales = "1000, 5000, 15000"
+
+ # Fill scales
+ lizmap.dlg.list_map_scales.setText(scales)
+ lizmap.get_min_max_scales()
+ assert lizmap.dlg.minimum_scale.text() == "1000"
+ assert lizmap.dlg.maximum_scale.text() == "15000"
+ assert lizmap.dlg.list_map_scales.text() == scales
+
+ # Check new values in the output config
+ output = lizmap.project_config_file(LwcVersions.latest(), check_server=False, ignore_error=True)
+
+ # Check scales in the CFG
+ assert output["options"]["minScale"] == 1000
+ assert output["options"]["maxScale"] == 15000
+ assert output["options"]["mapScales"] == [1000, 5000, 15000]
+
+ # Project is in EPSG:2154, must be False
+ assert not output["options"]["use_native_zoom_levels"]
+
+ # Check an empty list and a populated list then
+ assert output["options"].get("acl") is None
+ lizmap.dlg.inAcl.setText("cadastre,urbanism")
+
+ output = lizmap.project_config_file(
+ LwcVersions.latest(),
+ check_server=False,
+ ignore_error=True,
+ )
+ TestCase.assertListEqual(["cadastre", "urbanism"], output["options"].get("acl"))
+
+def test_read_existing_lwc_3_6_to_3_7(data: Path, qgis_iface: QgisInterface):
+ """Test to read a CFG 3.6 and to export it to 3.7 about scales."""
+ # Checking CFG before opening the QGS file
+ with data.joinpath("3857_project_lwc_3_6.qgs.cfg").open() as f:
+ json_data = json.load(f)
+ TestCase.assertListEqual([1000, 5000, 10000, 500000], json_data["options"]["mapScales"])
+
+ project = QgsProject.instance()
+ project.read(str(data.joinpath("3857_project_lwc_3_6.qgs")))
+ assert len(project.mapLayers()) == 1
+
+ lizmap = Lizmap(qgis_iface, lwc_version=LwcVersions.Lizmap_3_7)
+ # read_cfg_file will call "layers_config_file"
+ lizmap.read_cfg_file(skip_tables=True)
+
+ assert lizmap.dlg.minimum_scale.text() == "1000"
+ assert lizmap.dlg.maximum_scale.text() == "500000"
+ assert lizmap.dlg.list_map_scales.text() == "1000, 5000, 10000, 500000"
+
+ output = lizmap.project_config_file(
+ LwcVersions.Lizmap_3_7,
+ check_server=False,
+ ignore_error=True,
+ )
+
+ # Project is in EPSG:3857, must be True
+ assert output["options"]["use_native_zoom_levels"]
+ # only two when we save
+ assert output["options"]["mapScales"] == [1000, 500000]
+ assert output["options"]["minScale"] == 1000
+ assert output["options"]["maxScale"] == 500000
+
+def test_read_existing_lwc_3_6_to_3_6(data: Path, qgis_iface: QgisInterface):
+ """Test to read a CFG 3.6 and to export it to 3.6 about scales."""
+ # Checking CFG before opening the QGS file
+ project = QgsProject.instance()
+ project.read(str(data.joinpath("3857_project_lwc_3_6.qgs")))
+ assert len(project.mapLayers()) == 1
+
+ lizmap = Lizmap(qgis_iface, lwc_version=LwcVersions.Lizmap_3_6)
+ # read_cfg_file will call "layers_config_file"
+ lizmap.read_cfg_file(skip_tables=True)
+
+ assert lizmap.dlg.minimum_scale.text() == "1000"
+ assert lizmap.dlg.maximum_scale.text() == "500000"
+ assert lizmap.dlg.list_map_scales.text() == "1000, 5000, 10000, 500000"
+
+ output = lizmap.project_config_file(
+ LwcVersions.Lizmap_3_6,
+ check_server=False,
+ ignore_error=True,
+ )
+
+ # Project is in EPSG:3857, must be False because of LWC 3.6
+ assert not output["options"]["use_native_zoom_levels"]
+
+ TestCase.assertListEqual([1000, 5000, 10000, 500000], output["options"]["mapScales"])
+ assert output["options"]["minScale"] == 1000
+ assert output["options"]["maxScale"] == 500000
+
+def test_atlas_auto_play_true_values(data: Path, qgis_iface: QgisInterface):
+ """Test some UI settings about boolean values."""
+ lizmap = _setup_empty_project(data, qgis_iface)
+
+ output = lizmap.project_config_file(LwcVersions.latest(), check_server=False, ignore_error=True)
+ assert output["options"].get("atlasAutoPlay") is None
+
+ lizmap.dlg.atlasAutoPlay.setChecked(True)
+
+ output = lizmap.project_config_file(LwcVersions.latest(), check_server=False, ignore_error=True)
+ assert output["options"].get("atlasAutoPlay")
+
+ lizmap.dlg.atlasAutoPlay.setChecked(False)
+
+ output = lizmap.project_config_file(
+ LwcVersions.latest(),
+ check_server=False,
+ ignore_error=True,
+ )
+
+ assert output["options"].get("atlasAutoPlay") is None
+
+def test_geolocation_values(data: Path, qgis_iface: QgisInterface):
+ """Test geolocation UI settings."""
+ lizmap = _setup_empty_project(data, qgis_iface)
+
+ # Default geolocation checkboxes checked
+ assert not lizmap.dlg.groupbox_geolocation.isChecked()
+ assert lizmap.dlg.checkbox_geolocation_precision.isChecked()
+ assert not lizmap.dlg.checkbox_geolocation_direction.isChecked()
+
+ # Default geolocation checkboxes enabled
+ assert lizmap.dlg.groupbox_geolocation.isEnabled()
+ assert not lizmap.dlg.checkbox_geolocation_precision.isEnabled()
+ assert not lizmap.dlg.checkbox_geolocation_direction.isEnabled()
+
+ # Check geolocation checkbox to enable other checkboxes
+ lizmap.dlg.groupbox_geolocation.setChecked(True)
+ assert lizmap.dlg.checkbox_geolocation_precision.isEnabled()
+ assert lizmap.dlg.checkbox_geolocation_direction.isEnabled()
+
+ output = lizmap.project_config_file(LwcVersions.latest(), check_server=False, ignore_error=True)
+ # options
+ assert output["options"].get("geolocation")
+ assert output["options"].get("geolocationPrecision")
+ assert not output["options"].get("geolocationDirection")
+
+ # Check direction
+ lizmap.dlg.checkbox_geolocation_direction.setChecked(True)
+ # Uncheck precision
+ lizmap.dlg.checkbox_geolocation_precision.setChecked(False)
+
+ output = lizmap.project_config_file(LwcVersions.latest(), check_server=False, ignore_error=True)
+ # options
+ assert output["options"].get("geolocation")
+ assert not output["options"].get("geolocationPrecision")
+ assert output["options"].get("geolocationDirection")
+
+
+def test_exclude_basemaps_from_single_wms_values(data: Path, qgis_iface: QgisInterface):
+ """Test exclude basemaps from single WMS UI settings."""
+ lizmap = _setup_empty_project(data, qgis_iface)
+
+ # Default checkbox states
+ assert not lizmap.dlg.checkbox_wms_single_request_all_layers.isChecked()
+ assert not lizmap.dlg.checkbox_exclude_basemaps_from_single_wms.isChecked()
+
+ # Exclude basemaps checkbox should be disabled when single WMS is off
+ assert not lizmap.dlg.checkbox_exclude_basemaps_from_single_wms.isEnabled()
+
+ # Enable single WMS - exclude basemaps checkbox should become enabled
+ lizmap.dlg.checkbox_wms_single_request_all_layers.setChecked(True)
+ assert lizmap.dlg.checkbox_exclude_basemaps_from_single_wms.isEnabled()
+
+ output = lizmap.project_config_file(LwcVersions.latest(), check_server=False, ignore_error=True)
+ # Single WMS enabled, exclude basemaps still unchecked
+ assert output["options"].get("wms_single_request_for_all_layers")
+ assert not output["options"].get("exclude_basemaps_from_single_wms")
+
+ # Enable exclude basemaps
+ lizmap.dlg.checkbox_exclude_basemaps_from_single_wms.setChecked(True)
+
+ output = lizmap.project_config_file(LwcVersions.latest(), check_server=False, ignore_error=True)
+ # Both options enabled
+ assert output["options"].get("wms_single_request_for_all_layers")
+ assert output["options"].get("exclude_basemaps_from_single_wms")
+
+ # Disable single WMS - exclude basemaps checkbox should become disabled
+ lizmap.dlg.checkbox_wms_single_request_all_layers.setChecked(False)
+ assert not lizmap.dlg.checkbox_exclude_basemaps_from_single_wms.isEnabled()
+
+
+def test_group_popup_by_layer(data: Path, qgis_iface: QgisInterface):
+ """Test group popup by layer UI settings."""
+ lizmap = _setup_empty_project(data, qgis_iface)
+
+ # Default checkbox states
+ assert not lizmap.dlg.checkbox_group_popup_by_layer.isChecked()
+
+ # Enable group popup by layer
+ lizmap.dlg.checkbox_group_popup_by_layer.setChecked(True)
+ assert lizmap.dlg.checkbox_group_popup_by_layer.isEnabled()
+
+ output = lizmap.project_config_file(LwcVersions.latest(), check_server=False, ignore_error=True)
+ # Group popup by layers option should be checked in output config file
+ assert output["options"].get("group_popup_by_layer")
+
+ # Disable group popup by layer
+ lizmap.dlg.checkbox_group_popup_by_layer.setChecked(False)
+ output = lizmap.project_config_file(LwcVersions.latest(), check_server=False, ignore_error=True)
+ # Group popup by layers option should be unchecked in output config file
+ assert not output["options"].get("group_popup_by_layer")
+
+
+def test_short_link_permalink(data: Path, qgis_iface: QgisInterface):
+ """Test short link permalink UI settings."""
+ lizmap = _setup_empty_project(data, qgis_iface)
+
+ # Default checkbox states
+ assert not lizmap.dlg.checkbox_short_link_permalink.isChecked()
+
+ # Enable short link permalink
+ lizmap.dlg.checkbox_short_link_permalink.setChecked(True)
+ assert lizmap.dlg.checkbox_short_link_permalink.isEnabled()
+
+ output = lizmap.project_config_file(LwcVersions.latest(), check_server=False, ignore_error=True)
+ # Short link permalink option should be checked in output config file
+ assert output["options"].get("short_link_permalink")
+
+ # Disable short link permalink
+ lizmap.dlg.checkbox_short_link_permalink.setChecked(False)
+ output = lizmap.project_config_file(LwcVersions.latest(), check_server=False, ignore_error=True)
+ # Short link permalink option should be unchecked in output config file
+ assert not output["options"].get("short_link_permalink")
diff --git a/uv.lock b/uv.lock
index 48061690..93a8a6e3 100644
--- a/uv.lock
+++ b/uv.lock
@@ -4,7 +4,8 @@ requires-python = ">=3.10"
resolution-markers = [
"python_full_version >= '3.15'",
"python_full_version >= '3.12' and python_full_version < '3.15'",
- "python_full_version < '3.12'",
+ "python_full_version == '3.11.*'",
+ "python_full_version < '3.11'",
]
[[package]]
@@ -215,6 +216,133 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
+[[package]]
+name = "coverage"
+version = "7.14.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/54/fd/0ab2772530e946e1be1abd0bc09e647ec9b02e88f0867857601fefca8953/coverage-7.14.1.tar.gz", hash = "sha256:30c08f7d90415aa98b3c990385dea2939b0da55f38515e5b369b83655f8523be", size = 920132, upload-time = "2026-05-26T20:41:36.783Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/92/69/0d2ef01ff4b8fcecd4cba920d11e92fa4f96ae412441d3b56a90a258e69b/coverage-7.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3e3680291c4a1d0dadfa84a2c459576a4af5133abb617905714339a0c73138cf", size = 219722, upload-time = "2026-05-26T20:38:14.002Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/ae/9afdeaa31b9d9ce98124b6abf8bb49119bf71aecae04f8567c189d91299f/coverage-7.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a5274669f37f2343635a347b91a60777621341ab3378e9c6ac9335eee704bddf", size = 220240, upload-time = "2026-05-26T20:38:17.424Z" },
+ { url = "https://files.pythonhosted.org/packages/51/69/c998589871df7ea7dba865cc5ee32b5a3e1d47ba6c68ef91104c7c46fa5e/coverage-7.14.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cfe5a5fec635799ef33428f1e5e61bafa45a92a96190ba731561ba558ccc214d", size = 246981, upload-time = "2026-05-26T20:38:19.266Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/10/1c7d04c13040dac531d21b712bbe08f902e6dd9b58f5d77875c4d030f8f2/coverage-7.14.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:62a9f70b52e0b5a95cfef4a5c5641b06983cadc5e538a3feeb5c00211f523ac2", size = 248812, upload-time = "2026-05-26T20:38:20.75Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/65/2a38a4607ef27cadcfbcee034dba5830ae2569f90144a0f4c7dbf47d30b0/coverage-7.14.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c18ebc343e15be53049b3a2dce38fe82d58f37e20ab9094b3a39c0aa4f6bb47", size = 250675, upload-time = "2026-05-26T20:38:22.159Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/a2/a446ed9752a4a59b79e0fb6cbb319f6facb2183045c0725462625e66f87e/coverage-7.14.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b84ffdf877644e7096aa936991efeed873f7f3df57b9cd001312b7668ab08550", size = 252590, upload-time = "2026-05-26T20:38:23.63Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/fd/e81fbd7ba752365546e9842b1cbdaad3d6919d2a522c590aef16a281ec5e/coverage-7.14.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e854312c4103f2ad4c0dc023b69b77ebfd2c89db5f86c4c94dc2353f9a92167e", size = 247691, upload-time = "2026-05-26T20:38:25.057Z" },
+ { url = "https://files.pythonhosted.org/packages/53/35/f3c26fdaae9ea937d154ca4d372e5ea0a4167ff70d36c6074ac2eacb2f83/coverage-7.14.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c643734307300234fafa36bf2a040a7235f8f177ea1fd6ec1423aea6fb7b929f", size = 248716, upload-time = "2026-05-26T20:38:26.406Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/14/940b6c49551fd343e8507ee2b0ba7af5d0aa04ed5bf768285cb7c72a9884/coverage-7.14.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:84ac9499e48700399a5dd0ea7085b5091961fec52c68d66b4ec0d3cf7f4441b1", size = 246721, upload-time = "2026-05-26T20:38:28.282Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/2c/40fc0634186c28292a662dff578866b3913983d6c375a3c2a74020938719/coverage-7.14.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:7f02d09f70776579b926d889a4c9c235070a1f47c40458aeaca563fae5acfdb5", size = 250533, upload-time = "2026-05-26T20:38:29.753Z" },
+ { url = "https://files.pythonhosted.org/packages/de/e3/2c26bf1e811f9df991ff2a9bdddebdd13ee0665d564df7d05979f9146297/coverage-7.14.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ce66d8e46da2bb5ee313a745cbd2e391d319176c1f7a9451bfcd3a2fb920859b", size = 246990, upload-time = "2026-05-26T20:38:31.516Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/b0/060260ef56bd92363ebdce0c7095ce422b06e69aae71828efeca473ab1ca/coverage-7.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c912c259304cfb5ee584481cfb7ce1ff932b4d61e6c9140b8f19cb7b5ed82332", size = 247593, upload-time = "2026-05-26T20:38:33.065Z" },
+ { url = "https://files.pythonhosted.org/packages/63/f3/501502046efeb0d6d94b5ca54941d95f1184183dd6bdb7f283985783bb4a/coverage-7.14.1-cp310-cp310-win32.whl", hash = "sha256:1238cb94638e610e972c60dac68e813f868dc7d6e982535270558443058d9d59", size = 222330, upload-time = "2026-05-26T20:38:35.36Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/5d/1bf99f2c558f128faf7906817ccbdb576ba815d3b41ce2ac1719b70a3663/coverage-7.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:fc459e5d73be2d6332fcfe8dbf3d8994671fe33c700f4565988ecfa511547253", size = 223261, upload-time = "2026-05-26T20:38:37.196Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/d7/477ad149490e6cb849f28abea1dabb9c823cea72e7500c81b4240ce619c0/coverage-7.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:478b5bcd63c2e1357c5c7e16c070690df7b07f676b1c114d7b93e533c664309f", size = 219848, upload-time = "2026-05-26T20:38:38.715Z" },
+ { url = "https://files.pythonhosted.org/packages/91/82/a5eb47257c50601bb7b9a9d2857c67b7a3a85ad74180eb2c98bb1fbe0ce5/coverage-7.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a24a81f9715ee42ef59a316cc11611c98fe23920f7c81861315c9f3ff4a230f4", size = 220354, upload-time = "2026-05-26T20:38:40.232Z" },
+ { url = "https://files.pythonhosted.org/packages/43/8b/78419b5391a5cb706b6544390507e469d83ffc9a8248b02c4011aceb9365/coverage-7.14.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:196a13319ad88d6d8ef5ab489ec4f44ddde2143c0c7d5b27786f6c3ffd56a7e1", size = 250771, upload-time = "2026-05-26T20:38:41.782Z" },
+ { url = "https://files.pythonhosted.org/packages/77/63/e77aaacd491182210d639636b7a8bba23ffffa9b82aa3762da9431855fa9/coverage-7.14.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3d452fd08b5c72c5167c93e6867b5c08500bd40f2a21e1e854a500550b6cc36f", size = 252683, upload-time = "2026-05-26T20:38:43.305Z" },
+ { url = "https://files.pythonhosted.org/packages/65/1c/a022e3cfbec2ac241640003cb3a817e161d9c7f5aa9b49173756cdc03204/coverage-7.14.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23bf7fa51ac02e07fc7c96849b82946da47ae862dc8f86d183b2a4864fc38129", size = 254791, upload-time = "2026-05-26T20:38:45.361Z" },
+ { url = "https://files.pythonhosted.org/packages/61/d6/967e408aca4c1ceb88cb0cc677169110ae7f5995fb5eaf5fb1f5a1bb8f5d/coverage-7.14.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bcaa50684dcaadfa599ac48f81103c756d791cfd85c97203d2217c593d48b860", size = 256748, upload-time = "2026-05-26T20:38:46.91Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/be/869188f7fe28638078ec479331ace6dc5f7b40b7153eb616f47ab79404d8/coverage-7.14.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4ea1c034f95c9b056e856b794630b17f9fa3d57e4800ff1e503d3be0f9c9078c", size = 250907, upload-time = "2026-05-26T20:38:48.493Z" },
+ { url = "https://files.pythonhosted.org/packages/07/aa/adb7d3b4278d690e68703abcd76ab1b948242e3668d921711551b78f9ddb/coverage-7.14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c7e057326434e441306226fbeb5d1aaf14a2637efe97ba668306635835f32ad7", size = 252483, upload-time = "2026-05-26T20:38:50.074Z" },
+ { url = "https://files.pythonhosted.org/packages/43/61/331c74103c62dcb0c4b9b3a0de9a61aca016208b0a90f109592a9f9ecc28/coverage-7.14.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:59baf88468dbc8d63b1887afd92bda52e40bb1561696e5819670601403810cec", size = 250545, upload-time = "2026-05-26T20:38:51.613Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/b6/c5dae3c104d89be04828f61810e6b3473825482e4c288cc4ed04553e08ae/coverage-7.14.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d34d75f892b3ab73ba11cab5442cce7b3e168fd64162b16f0e1e0d09c508edef", size = 254310, upload-time = "2026-05-26T20:38:53.503Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/a1/2b9d5863e3b83c01ad8199e3c597802fbb3a9dc90b058885804c20296d31/coverage-7.14.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3a56abc20a472baf0304c455721bc601477440d28ecfde8a03dde79ede07e0df", size = 250266, upload-time = "2026-05-26T20:38:55.414Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/5e/0e511fbdb269359be26fe678a1c3fa1f2aa2a01573cc3f54268c8d6d4797/coverage-7.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6a3cb83d1552c0cd1b4906655b6a33fd4a8473229633a901c6b73bf86914dee9", size = 251174, upload-time = "2026-05-26T20:38:57.141Z" },
+ { url = "https://files.pythonhosted.org/packages/85/10/e55307b622b3dd9671cb321824502dc10f93e72f2802b9946159a8edadeb/coverage-7.14.1-cp311-cp311-win32.whl", hash = "sha256:10274a1fbeb8ec5d72966e17bb198a3104257aca4ac09d98667c5f8aca8c8548", size = 222354, upload-time = "2026-05-26T20:38:58.727Z" },
+ { url = "https://files.pythonhosted.org/packages/71/cf/107421693cfb71e4f1ca5bf70443f64d4161878068d07a3e51c7ad21d17b/coverage-7.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:87ebdf787d4888e3f3f2d523eadc6e18c6d18c6d0eb173801a189641627fb37e", size = 223290, upload-time = "2026-05-26T20:39:00.413Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/1d/3e3644585eb29e9dafefb19555078529a4d7cce12bd21929664eea989277/coverage-7.14.1-cp311-cp311-win_arm64.whl", hash = "sha256:dd34767fa19848d35659ffc0a75314f58c7af3f1cd87ec521e8292a1238398a3", size = 221953, upload-time = "2026-05-26T20:39:02.159Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/b7/bdbb725ba02c5b42825b200c940f38b7a54fcad24627b7192f78f8110d76/coverage-7.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a06c76364a9360e33d6d23769aefdf7f66f38e2ffb60ceb1baaa4989d83b695c", size = 220022, upload-time = "2026-05-26T20:39:03.702Z" },
+ { url = "https://files.pythonhosted.org/packages/72/81/fdc0898a55c6219223291ec1a1fe89966ef212ce82276aa0899df84b5de0/coverage-7.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fad54e871165f6ec2f536063ac74c3104508a12963e64072ba44bd822de52b0c", size = 220379, upload-time = "2026-05-26T20:39:05.381Z" },
+ { url = "https://files.pythonhosted.org/packages/de/72/de048c4a25e13bce59ac6a339351c10bdf2515e07459afcdaf04dc3143a2/coverage-7.14.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:84b535f00655ecafe1d929d1fb00ed5d6fa3051ea643ab2c161a3887b86f294b", size = 251888, upload-time = "2026-05-26T20:39:07.367Z" },
+ { url = "https://files.pythonhosted.org/packages/28/30/300c343f68beb9d4cbb64ec81e58c5b6b80b56927f72d2b38654ac26e013/coverage-7.14.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6b6b0853b895fe0e98cbfc580d1ec3393d9302b4b1e96a77b3f5c91fdab899e6", size = 254624, upload-time = "2026-05-26T20:39:09.037Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/ed/7b25642496e8170b6bac14adce00537c6e5fa2d586159401a4de3e8b49e6/coverage-7.14.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:442cc9c952b2df400cda54bb04ab87330cf2cd08a8692cbbea36773531eb6f37", size = 255739, upload-time = "2026-05-26T20:39:10.889Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/a2/abd210b8c4e29c24e4624916db97bb519097a91034aaeb767f937e7da794/coverage-7.14.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8270544c361ed405a27a060dbc9ed2c124b084d96dfdc2d9a2510482aef981ad", size = 257998, upload-time = "2026-05-26T20:39:12.722Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/24/7c50beed3792fe62f6ce0545c6686ce83379719e2c0276179333d97eae92/coverage-7.14.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:48b283b1dd6372e8de2a7a9a4c4d5dc06f4d4fd209b876f3c88a7a205a0c8f84", size = 252296, upload-time = "2026-05-26T20:39:14.259Z" },
+ { url = "https://files.pythonhosted.org/packages/15/05/0f874628ebcbfc77ead559ff210281ef06a97db08481832e7dd39274a135/coverage-7.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5b0c99ba93a07d56f6df340bb79be53202a082b2fdb81bfe6190b741a3470d54", size = 253658, upload-time = "2026-05-26T20:39:15.923Z" },
+ { url = "https://files.pythonhosted.org/packages/99/6f/ca6ad067364b337ef997802115e7ecad2abd2248b05471464b0dea02b4d4/coverage-7.14.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e471bc5769ff073b058cfadb0d736b56ce067c8560eabeb0da88462df98c23e7", size = 251803, upload-time = "2026-05-26T20:39:17.537Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/30/b9b4d377cd9f40baf228068f5a81faf8450c6228503011bd499708483a50/coverage-7.14.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f497a1ea81d4cd7c10ddcaa685135b9aabd291af3d55775a9ddf3cb7a364cdd9", size = 255873, upload-time = "2026-05-26T20:39:19.414Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/21/7c721a9e5e6bb88547d30a787aefb97512d3f54c1324c7488d9b3743f7f9/coverage-7.14.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2222be86d0b54f5dd5a38f45f17f315f737245e857bf0bdedc70734f84a13c02", size = 251372, upload-time = "2026-05-26T20:39:21.169Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/8c/f8ae5a2200130e1503cd7661a6cd3b2b7bacef98277fbf3571fb13f8b766/coverage-7.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:85e85586565842f6932abebd4c18bcb1074223dc0b3576e7d173ca710622813a", size = 253245, upload-time = "2026-05-26T20:39:23.097Z" },
+ { url = "https://files.pythonhosted.org/packages/34/62/70a9024672a5f6910517d9628c52c9afbdd3cf8f46426af52bb148a56fff/coverage-7.14.1-cp312-cp312-win32.whl", hash = "sha256:4a28fd227808366b196a75476dced2eb35b351d6766ba9c858dc93319e87f4f1", size = 222567, upload-time = "2026-05-26T20:39:24.868Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/81/8b7cd386839b039ebe1855733b9f9449a8dec5d79564018234f185a7fa70/coverage-7.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:54acdb6674a4661768d7bf7db32dfb9f46ab1d764f8aba6df75ce1a6a088724e", size = 223372, upload-time = "2026-05-26T20:39:26.603Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/ba/b44d472022f620d289d95fa830143235c0c36461c6f2437ea8d51e5481ed/coverage-7.14.1-cp312-cp312-win_arm64.whl", hash = "sha256:99cd41ff91afd94896fea3bc002706b6ae4ce95727d06e4a0f39c0a8d8bd8b1a", size = 221989, upload-time = "2026-05-26T20:39:28.242Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/9e/5f6d56327c62b185225d145191c607e07515294a0aa6338e58805cd4a5ac/coverage-7.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:be9f2c802dcfce3f71298303aa5dad0dce440a76c52f2f60dacd8656dab78793", size = 220044, upload-time = "2026-05-26T20:39:29.902Z" },
+ { url = "https://files.pythonhosted.org/packages/75/92/e82aca356744cbbc0f77a0b623e38918c1872361963413a3bab5d0340393/coverage-7.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6223a72fd0e4c7156353ec0f08a5f93623e1d3034d0e2683b9bb8ea674131b1d", size = 220412, upload-time = "2026-05-26T20:39:31.561Z" },
+ { url = "https://files.pythonhosted.org/packages/27/c9/385bde0bf7ed0f4bf3a7ee5367060a86b5d218718cfd6fb943c0f836b34f/coverage-7.14.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7279d2110a28cebc738b6459ecda2771735a4c18465fbbd36b3288fe5ed92247", size = 251412, upload-time = "2026-05-26T20:39:33.337Z" },
+ { url = "https://files.pythonhosted.org/packages/51/8c/23faf6a2343a0d17f960a4bd56c43bc7eb4cf312f774dd6ceebd82c7d8fc/coverage-7.14.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9eeb3fcbc13ba40dfbdb22d01d196a28e9cef9ed4c29b60061a1e0e823a9929d", size = 254008, upload-time = "2026-05-26T20:39:35.009Z" },
+ { url = "https://files.pythonhosted.org/packages/42/06/36f4aa9ca8a815e6036156e80706a67828bb97bd826948244f6996dda957/coverage-7.14.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f0cfc27c539f07cf5c0a4cfe211d0b6cae039f8f40526dbaa71944e64b50a7b", size = 255241, upload-time = "2026-05-26T20:39:36.71Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/79/95266316352f90f6b1c6736bb413302edfde2453fb32422d3911642691b3/coverage-7.14.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:221c70f316241a78e77e607c227cefc8808d4e08f28d99c04f35694690e940be", size = 257373, upload-time = "2026-05-26T20:39:38.412Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/9c/58316d1f66c488b5fca8a0eb3e98348807813efa8a0d0833b9021be27488/coverage-7.14.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:da028256b04ec30e5e0114b6f76172938c313991f0a2d3d894271315cf5d5e43", size = 251635, upload-time = "2026-05-26T20:39:40.268Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/5a/ca2398a568e16fed7bb713e84ba3603a7164fb65779abe645c565ec890d5/coverage-7.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76a085d7005236a767e3426148b2c407e53ad61695c562f8a81da2d373324901", size = 253373, upload-time = "2026-05-26T20:39:42.145Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/2c/0396562c32deaebe7be51d865b3a41e9a87d7561acafe1a28f53b07e019a/coverage-7.14.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b553d04b5e778a8e56d57eb134aff42a92718ecba45e79c4764ecfa40efd92ff", size = 251341, upload-time = "2026-05-26T20:39:43.907Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/8f/a94f9221184c9cae1ee115820e3798e48b6b17777a9f19e46fb9a0c8dc74/coverage-7.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:46f714d2fb8ae2f4f29f23ada7f1e79b759fff5a70f94a1dac23af204c3ec9e4", size = 255497, upload-time = "2026-05-26T20:39:46.166Z" },
+ { url = "https://files.pythonhosted.org/packages/71/69/505d70e47db1eaebcd002c39759707621ef184cd6b1ae084d9f41293f323/coverage-7.14.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1896f5e19ff3f0431c7ce2172adc54890fd97f86b59ced8ca1649145d9ffe35d", size = 251159, upload-time = "2026-05-26T20:39:48.03Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/aa/58681c383aa33a9d2ed40a02d7a22fbf780d1fa4d575396365777828198c/coverage-7.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:62fd185ef9df3c33d1c8178c5af105f762afbad96038de9a4ae100aa6297ca33", size = 252934, upload-time = "2026-05-26T20:39:49.872Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/fd/11c928cd6bdffc7074bb5965c173d9ebf517fb00205e1da524b98d29ef92/coverage-7.14.1-cp313-cp313-win32.whl", hash = "sha256:ab4af6352741a604c431c6072fce5bee33bf0f20dc7a56618d6bf6bb89e9810c", size = 222584, upload-time = "2026-05-26T20:39:51.68Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/92/fb416fc26d340dcba19518c418d6048e913186e17243982c5e435e41fa7a/coverage-7.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:7af486dabe8954d03b087f0021540897afe084f04e16ff5579e08cc46f871416", size = 223394, upload-time = "2026-05-26T20:39:53.472Z" },
+ { url = "https://files.pythonhosted.org/packages/73/c6/02d56e3867972f77d5036de924643f26c056e848f00452cafb4dbc3c29b4/coverage-7.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:2224f89ffd0c5605ccce1ed7a584da162bc7c55f601ab1c946bc9de31a486b42", size = 222015, upload-time = "2026-05-26T20:39:55.374Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/9e/fcc77914050df73f7662fa1f00902774c79c075a8388ab334074574bf77e/coverage-7.14.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:de286598cc65d2b489411174b1faec2f5a7775fb3201fd925db2a76b4030f37d", size = 220733, upload-time = "2026-05-26T20:39:57.189Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/67/2963cbdaf5cbadec44efa3a1e39eaa1f02df4079585f05387607a221e126/coverage-7.14.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:042c46ded7c288aeb07cf14a28b6c1e10b78fcba40171c3fa1e939377eeef0b5", size = 221086, upload-time = "2026-05-26T20:39:59.019Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/c5/8701645574e11881f2f47d8930f98bc48b5d43b25eb5b4430dfc4a2f9f48/coverage-7.14.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f4ddbe407477f04c45115d1a4e5bc480f753553b534d338d4c3358b1cdd0ea52", size = 262381, upload-time = "2026-05-26T20:40:00.822Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/28/7a64d73598263e0c5abd5084211a8474488d31b3c552ff531c719dfcff62/coverage-7.14.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d13e6725992e2d2fd7d81d4f5241952d13740121dfd501da09201be39b2c003a", size = 264458, upload-time = "2026-05-26T20:40:02.506Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/d8/4969179db9f7eb4df218e69540adf829d1c835f59452513d065d15446802/coverage-7.14.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f747dc8edcfe740130f28f32f3995e955494285717e86ee25af51db2219df08a", size = 266884, upload-time = "2026-05-26T20:40:04.421Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/78/a45d5794dbc9bafd97afc96a4377c86c7820d78b6cf51b89bc1d4e919275/coverage-7.14.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced2f09ef276fd58611a1ef502164ad266d2b75174e5a40cabbdb4033f9f6cf2", size = 268022, upload-time = "2026-05-26T20:40:06.298Z" },
+ { url = "https://files.pythonhosted.org/packages/21/cb/4f5e354e9e3e67af96bd4e57113e6db6b22298c7168b13eec408a549903d/coverage-7.14.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b84800013769a78ccb9ef4659402e26d06867e337b61ec365f77ad008adea80e", size = 261631, upload-time = "2026-05-26T20:40:08.226Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/49/eced49af4cb996d5d8b7e94e736175c513e4facd3398507b89892b4326d8/coverage-7.14.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ea8cd6ca0ee9f616aaef3afc6882e32c2cbf18b00d96313ffd76af650574034d", size = 264443, upload-time = "2026-05-26T20:40:10.137Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/d8/5603a88a7c5913a6b54f6cb1a8c46f7b39cbb30f27cd3f492908da09b2d7/coverage-7.14.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:aa5e304a873fabddc11e484e9b6b738bd38bd7bed17b09aa84eecf5332e8b8bb", size = 262069, upload-time = "2026-05-26T20:40:11.999Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/59/2ae3cb79da554a06c8619d6c88ea19dd1e4aed4b834b6a83bb1fa243bdc5/coverage-7.14.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5a1c5215be81035e629d5bc756650634d0bf31991038db7a0eccb90f025ce16d", size = 265780, upload-time = "2026-05-26T20:40:13.858Z" },
+ { url = "https://files.pythonhosted.org/packages/af/5f/b130c1dc999031f2648bd25317fbce505ad8d5562079b4ed81e736a84967/coverage-7.14.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:79058c47dae6788504b5effb319961bcd72d7240551464b91d474bc0ed186d69", size = 260970, upload-time = "2026-05-26T20:40:16.142Z" },
+ { url = "https://files.pythonhosted.org/packages/87/d1/ec13ccddeb48ec963bdfa72a11224bac2584bd045ba13beca82f8113e9c7/coverage-7.14.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:370c5afae3fa0658e11694a32b24c2778f6bc2d17718121f94ee185e69f26b54", size = 263157, upload-time = "2026-05-26T20:40:18.382Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/c2/cd91ead503045161092d3845f7bb95ea2f25131ce96d3e314dd835d91b9c/coverage-7.14.1-cp313-cp313t-win32.whl", hash = "sha256:3758dd0a7f1fa57365ef2e781df0f0731d38b6e3772259d13dae4bd8a958d4b1", size = 223259, upload-time = "2026-05-26T20:40:20.381Z" },
+ { url = "https://files.pythonhosted.org/packages/71/9f/1e28d97e6bd2c76b07f38b7c02870f1371255ff6717f54eca578fcbbdd0e/coverage-7.14.1-cp313-cp313t-win_amd64.whl", hash = "sha256:6ff665fb023a77386fe11685190cee1f60a7d635994a30d9b0a061533d470fce", size = 224320, upload-time = "2026-05-26T20:40:22.316Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/e0/d936e908f0e1efa55e52b91e01b52f1055cef5e1ab2718493390ed8e2fb8/coverage-7.14.1-cp313-cp313t-win_arm64.whl", hash = "sha256:17a5a241e5997621a956a7f402a7433ef4221e5152809b785bec79e2323799f1", size = 222577, upload-time = "2026-05-26T20:40:24.894Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/34/fc2f101b151af3799a101f0550b0454aa008afdc0add677394ec4aa8ea10/coverage-7.14.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d5ed429d0b8edaac649e889b4ffcedb6c80b06629a3f93050e3dddfb99235bee", size = 220091, upload-time = "2026-05-26T20:40:27.249Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/a7/1ebae2ab5b961b5c79bb09fe7b3ac99edb190d8be4a8c510b2cf66f46468/coverage-7.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8011224a62280e50dab346960c03cf47aca1a1e09e608c0fb33fd6e0cc8e9500", size = 220421, upload-time = "2026-05-26T20:40:30.084Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/90/92aca9cf0acc95123c96cd1eb1f08917897a7f5dee01e15738922971ec31/coverage-7.14.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:12c42ec1e14f553c4f817e989365982e646e27211f10a0f717855b94a79c8906", size = 251466, upload-time = "2026-05-26T20:40:32.542Z" },
+ { url = "https://files.pythonhosted.org/packages/26/2b/78048cbe3b999f6cbf9cc0d90abba6a88a3e0863a8c1c6cbc762f3f8802f/coverage-7.14.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:06144cd511cf2624873a035c5069cf297144f6e77a73ee3d7a55b605ec5efb42", size = 253973, upload-time = "2026-05-26T20:40:34.473Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/21/c2e33b29d1cfde484a19d437afc343c6cd30b08d78cbbf9f5aff14e57b2b/coverage-7.14.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a311d8e1da24be5c1ccf85cbfb06315dbaa1703d5a1eab3f6432c72b837917c8", size = 255318, upload-time = "2026-05-26T20:40:38.154Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/ee/aad2f108d63b769121005302f16bf66db8625c88ceaba466942e09a2607e/coverage-7.14.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c79cead5b5bc584d9c71451cb984d0e3a84e0c0937379c8efcbf27c8d661b851", size = 257633, upload-time = "2026-05-26T20:40:40.164Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/f8/11a2c29b4fd76d9849f81d0bb812ec0017a9396df3217214e38934a8c837/coverage-7.14.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dcbf65f1f66a26cdd88c35cf68fb4729c5d1cd2e88added72420541dfb212034", size = 251488, upload-time = "2026-05-26T20:40:42.631Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/b8/9a5820de4b8ac2b71d85e3b5fb49108d7469c665f0e2ad0dd7569023e305/coverage-7.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fd86572566fb40189a8260446158235159bc7a82dfbc87a3b39cf4fb57fcec1c", size = 253329, upload-time = "2026-05-26T20:40:45.208Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/ff/f33e4823667e27548e8fd8df44217515303f9808d0ff29817db56f87d990/coverage-7.14.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:7771b601718fdde84832c3a434ca9bbf4ae9adbc49d84198b4110700c3c77c36", size = 251291, upload-time = "2026-05-26T20:40:47.502Z" },
+ { url = "https://files.pythonhosted.org/packages/68/9b/489db0ebb209054766b90a9014a45f6d26eb724c02ec21311c3733b5a644/coverage-7.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:39b21e212c55af06fa375e3dbf90a8a8e38792f3a910c580066d23563830ddd5", size = 255564, upload-time = "2026-05-26T20:40:49.372Z" },
+ { url = "https://files.pythonhosted.org/packages/27/b5/16bc2d4c2409b23c7737edb68c83bc89e345f378050549fe1d75ac7d34d5/coverage-7.14.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f2302660e32562a532b442480121aef8aa61a5bdb20b30bf0adab29f10a5a4b4", size = 251107, upload-time = "2026-05-26T20:40:51.677Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/0c/2629997469a00cd069d588a41c9dc887610f2775ae89d250c4791e65272a/coverage-7.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:03a6f93c1ec3b7f2e77b5dbcc5573a2c21f12529a5c6bbe0f16f72303cc2fa4d", size = 252764, upload-time = "2026-05-26T20:40:54.267Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/ee/f78d63c8f079e0d7211c7e2401fa17e311514534ba61bae03e4b287ce4ab/coverage-7.14.1-cp314-cp314-win32.whl", hash = "sha256:8a3ce026d73290f42f08dafecbd82c193a74df280461fbf97300fec51fd133ee", size = 222837, upload-time = "2026-05-26T20:40:56.496Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/b9/be539854f93a70dfbeec69117f33ec70dc42ff0b65b5b07ab8d40d04228e/coverage-7.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:114c95ef29302423b87d159075805f4ab973254a2638a5d7d046c94887cc87d7", size = 223650, upload-time = "2026-05-26T20:40:58.351Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/9e/24e2842fef40f35ac82ba3a7719c8023d011bf3bf652d0675316a9d088a1/coverage-7.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:a07891c3f4805442b31b71e84ba3cf29ed1aa9a428284e06deeb4b23e5b46343", size = 222218, upload-time = "2026-05-26T20:41:00.321Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/1d/ac0a9df5fe31c1e8bdd658074905fc12844a05c1a7e3fdb8417e97c31e23/coverage-7.14.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1101a5ebb083aecb625ebb6209d4105b58f647b093cb2dc8122d7b33f743cfe1", size = 220822, upload-time = "2026-05-26T20:41:02.281Z" },
+ { url = "https://files.pythonhosted.org/packages/32/cf/f964fd9aff20323f9f1a726c97135f8a76bcd87b92dad141a456a43f3c64/coverage-7.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:851b9e1e4e8a4608e77c79714b2e77c0970d2ed7202a05e92ae407817481887b", size = 221084, upload-time = "2026-05-26T20:41:04.593Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/5e/7e5ef2aba844de2b80d678619fcf0841b42e3f37f16411226f3fe4c1016f/coverage-7.14.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d5b89cdfb2ee051b71e8c3c70bd81a9eff81100f736a269136fe1a68efe00474", size = 262454, upload-time = "2026-05-26T20:41:06.641Z" },
+ { url = "https://files.pythonhosted.org/packages/64/62/75809bded87015cc4935524218a2a8ed8dd1a8498bfed30a2f4f7a4b4d34/coverage-7.14.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0177614a0370f227888b4e436a7c55686d6a9f90eb1ade2b624ba685a1686e86", size = 264578, upload-time = "2026-05-26T20:41:08.556Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/42/d33392dc14633525012d2d504fa1a33b05538bf535f5c1d64675e5754b78/coverage-7.14.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d69af5dea2de76fc485a83032a630523f985198b7e25be901ec60181587b01e", size = 266981, upload-time = "2026-05-26T20:41:10.824Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/49/0157c4428c2aca7f1e09d5565930586fd5ae36f1655f08b0daa7cf1fcae1/coverage-7.14.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:35ab22d91de736e8966b980dc355cbcdd2c6dbbcfe275f9a2991bc8a91b3df65", size = 268112, upload-time = "2026-05-26T20:41:12.966Z" },
+ { url = "https://files.pythonhosted.org/packages/96/26/86b9ce71f4092b1ed325ce1421698081df1286b833400b6836912834d6e0/coverage-7.14.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:357d4e32935c36588aaba057d734fa32428c360c9fc2e4442afbf1b646beee6e", size = 261558, upload-time = "2026-05-26T20:41:15Z" },
+ { url = "https://files.pythonhosted.org/packages/20/4c/c311210c5472cf5401d8422b0d7812cdd520f24417673afabda6c323faca/coverage-7.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:51bd64741cc6fa065abd300ede1afe5a5291ece9c31da8b24884deda48bcc3f8", size = 264447, upload-time = "2026-05-26T20:41:17.369Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/71/59513f8710ed3e6b0ac0a050a5b7e977bb9c9e880354863b5d00d8809256/coverage-7.14.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:9132cd363a68a4c3daa7c8704a654b1e39d3360f6f5b8ddd470608a945236c07", size = 262048, upload-time = "2026-05-26T20:41:19.309Z" },
+ { url = "https://files.pythonhosted.org/packages/84/8d/bceed32dc494f5bbf50f775cd2e78ca814953942b5ea28d3c1c3ac316f14/coverage-7.14.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:07c6290b1697b862c0478eab545eec949a0d0e4d6d03497f446d706da3b4f2de", size = 265781, upload-time = "2026-05-26T20:41:21.559Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/c5/9348fe40dbfd4991aaf78df2c6c3098bfb2cc834d1fd362a64b4efef855a/coverage-7.14.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5ea0c297e27133853b4d8a3eb799bff5a2dbd9f2f41537a240d337ac9b4df890", size = 260896, upload-time = "2026-05-26T20:41:23.428Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/92/1ea0f03929da7cf87206b1fa24f4c8e9c158be0455481af29ec0a1f3503f/coverage-7.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:01b7733daad0237daa01ef80fe2dfceffc911e6a17fa7b55d14aa8214eaaaecd", size = 263214, upload-time = "2026-05-26T20:41:25.419Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/a9/b2493c054c0e01a643266742ab45e15744e60743f9260cd930c7142b1124/coverage-7.14.1-cp314-cp314t-win32.whl", hash = "sha256:6adc5a36984624a70bf11d7184e20fa0a49aa7c47ffab43804106a1a695ea22e", size = 223624, upload-time = "2026-05-26T20:41:27.795Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/bd/3e1e6a57fccd2d7c83fcdf338e93ba98eb85c6e877dd34731ac585375490/coverage-7.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:ddf799247318f34dbcd2efa8c95a8d0642674e926bb1774cf9b63dfd2a389d1c", size = 224728, upload-time = "2026-05-26T20:41:30.098Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/d7/31066cf1d2f0c6c797fce911bcfa01dd35642dc6da992a950256097c5860/coverage-7.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:145986fe66647eb489f18d9a997567a3fd358584c4b5a808769113abc07466af", size = 222752, upload-time = "2026-05-26T20:41:32.123Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/3c/1a983b9a745d7f83d53f057bcc5bf79ba6a2bbc08266b3f0c7d6fe630c9b/coverage-7.14.1-py3-none-any.whl", hash = "sha256:a252f21c27e38347e60111a3266b03827422a7d5525951aceee313aa68bab1d2", size = 211815, upload-time = "2026-05-26T20:41:34.078Z" },
+]
+
+[package.optional-dependencies]
+toml = [
+ { name = "tomli", marker = "python_full_version <= '3.11'" },
+]
+
+[[package]]
+name = "decorator"
+version = "5.3.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/60/8b/32f9823da46cde7df2087faa08cd98d01b908f8dcab982cdba9c84e85355/decorator-5.3.1.tar.gz", hash = "sha256:4cbcdd55a6efadb9dbea26b858f4fb3264567b52d69ca0d25b721b553f60ea82", size = 58084, upload-time = "2026-05-18T06:03:28.057Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl", hash = "sha256:f47fe6fdbd2edd623ecfe36875d37aba411624e2670dd395dddae1358689bb3c", size = 10365, upload-time = "2026-05-18T06:03:26.517Z" },
+]
+
[[package]]
name = "detect-secrets"
version = "1.5.0"
@@ -240,6 +368,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
]
+[[package]]
+name = "executing"
+version = "2.2.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" },
+]
+
[[package]]
name = "future"
version = "1.0.0"
@@ -267,6 +404,83 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
]
+[[package]]
+name = "ipython"
+version = "8.39.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.11'",
+]
+dependencies = [
+ { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" },
+ { name = "decorator", marker = "python_full_version < '3.11'" },
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+ { name = "jedi", marker = "python_full_version < '3.11'" },
+ { name = "matplotlib-inline", marker = "python_full_version < '3.11'" },
+ { name = "pexpect", marker = "python_full_version < '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
+ { name = "prompt-toolkit", marker = "python_full_version < '3.11'" },
+ { name = "pygments", marker = "python_full_version < '3.11'" },
+ { name = "stack-data", marker = "python_full_version < '3.11'" },
+ { name = "traitlets", marker = "python_full_version < '3.11'" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/40/18/f8598d287006885e7136451fdea0755af4ebcbfe342836f24deefaed1164/ipython-8.39.0.tar.gz", hash = "sha256:4110ae96012c379b8b6db898a07e186c40a2a1ef5d57a7fa83166047d9da7624", size = 5513971, upload-time = "2026-03-27T10:02:13.94Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c0/56/4cc7fc9e9e3f38fd324f24f8afe0ad8bb5fa41283f37f1aaf9de0612c968/ipython-8.39.0-py3-none-any.whl", hash = "sha256:bb3c51c4fa8148ab1dea07a79584d1c854e234ea44aa1283bcb37bc75054651f", size = 831849, upload-time = "2026-03-27T10:02:07.846Z" },
+]
+
+[[package]]
+name = "ipython"
+version = "9.14.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.15'",
+ "python_full_version >= '3.12' and python_full_version < '3.15'",
+ "python_full_version == '3.11.*'",
+]
+dependencies = [
+ { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" },
+ { name = "decorator", marker = "python_full_version >= '3.11'" },
+ { name = "ipython-pygments-lexers", marker = "python_full_version >= '3.11'" },
+ { name = "jedi", marker = "python_full_version >= '3.11'" },
+ { name = "matplotlib-inline", marker = "python_full_version >= '3.11'" },
+ { name = "pexpect", marker = "python_full_version >= '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
+ { name = "prompt-toolkit", marker = "python_full_version >= '3.11'" },
+ { name = "psutil", marker = "python_full_version >= '3.11' and sys_platform != 'emscripten'" },
+ { name = "pygments", marker = "python_full_version >= '3.11'" },
+ { name = "stack-data", marker = "python_full_version >= '3.11'" },
+ { name = "traitlets", marker = "python_full_version >= '3.11'" },
+ { name = "typing-extensions", marker = "python_full_version == '3.11.*'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/21/c2/c0064cf15d026501a1ef70e42efd9c3f818663089399aacc5e37a82901c1/ipython-9.14.0.tar.gz", hash = "sha256:6f27ff0f1d9ea050e0551f71568bc4b34d8aba579e8f111c5b4175f44ac6b4aa", size = 4432601, upload-time = "2026-05-29T15:13:24.611Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/14/a3/9e59340f02c1dc8f8c0a05b09244712b8609eb5439f9996e887e2b82f452/ipython-9.14.0-py3-none-any.whl", hash = "sha256:8fd984a3372c14b12790b084ba6b5cff5678c0cb063244a0034f06a51f20d6c2", size = 627457, upload-time = "2026-05-29T15:13:22.942Z" },
+]
+
+[[package]]
+name = "ipython-pygments-lexers"
+version = "1.1.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pygments", marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" },
+]
+
+[[package]]
+name = "jedi"
+version = "0.20.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "parso" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/46/b7/a3635f6a2d7cf5b5dd98064fc1d5fbbafcb25477bcea204a3a92145d158b/jedi-0.20.0.tar.gz", hash = "sha256:c3f4ccbd276696f4b19c54618d4fb18f9fc24b0aef02acf704b23f487daa1011", size = 3119416, upload-time = "2026-05-01T23:38:47.814Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl", hash = "sha256:7bdd9c2634f56713299976f4cbd59cb3fa92165cc5e05ea811fb253480728b67", size = 4884812, upload-time = "2026-05-01T23:38:43.919Z" },
+]
+
[[package]]
name = "librt"
version = "0.11.0"
@@ -360,11 +574,14 @@ source = { virtual = "." }
[package.dev-dependencies]
dev = [
{ name = "bandit" },
+ { name = "coverage", extra = ["toml"] },
{ name = "detect-secrets" },
{ name = "mypy" },
{ name = "psycopg", extra = ["binary"] },
{ name = "pytest" },
+ { name = "pytest-qgis" },
{ name = "ruff" },
+ { name = "semver" },
{ name = "vulture" },
{ name = "webdavclient3" },
]
@@ -381,19 +598,28 @@ packaging = [
tests = [
{ name = "psycopg", extra = ["binary"] },
{ name = "pytest" },
+ { name = "pytest-qgis" },
+ { name = "semver" },
{ name = "webdavclient3" },
]
+tools = [
+ { name = "ipython", version = "8.39.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+ { name = "ipython", version = "9.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+]
[package.metadata]
[package.metadata.requires-dev]
dev = [
{ name = "bandit" },
+ { name = "coverage", extras = ["toml"] },
{ name = "detect-secrets" },
{ name = "mypy" },
{ name = "psycopg", extras = ["binary"] },
- { name = "pytest", specifier = ">=5.3" },
+ { name = "pytest" },
+ { name = "pytest-qgis", marker = "python_full_version >= '3.10'", git = "https://github.com/3liz/pytest-qgis.git" },
{ name = "ruff" },
+ { name = "semver" },
{ name = "vulture" },
{ name = "webdavclient3" },
]
@@ -407,9 +633,12 @@ lint = [
packaging = [{ name = "qt-transifex", marker = "python_full_version >= '3.12'", git = "https://github.com/3liz/qt-transifex" }]
tests = [
{ name = "psycopg", extras = ["binary"] },
- { name = "pytest", specifier = ">=5.3" },
+ { name = "pytest" },
+ { name = "pytest-qgis", marker = "python_full_version >= '3.10'", git = "https://github.com/3liz/pytest-qgis.git" },
+ { name = "semver" },
{ name = "webdavclient3" },
]
+tools = [{ name = "ipython" }]
[[package]]
name = "lxml"
@@ -541,6 +770,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" },
]
+[[package]]
+name = "matplotlib-inline"
+version = "0.2.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "traitlets" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/bd/c0/9f7c9a46090390368a4d7bcb76bb87a4a36c421e4c0792cdb53486ffac7a/matplotlib_inline-0.2.2.tar.gz", hash = "sha256:72f3fe8fce36b70d4a5b612f899090cd0401deddc4ea90e1572b9f4bfb058c79", size = 8150, upload-time = "2026-05-08T17:33:33.49Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl", hash = "sha256:3c821cf1c209f59fb2d2d64abbf5b23b67bcb2210d663f9918dd851c6da1fcf6", size = 9534, upload-time = "2026-05-08T17:33:32.055Z" },
+]
+
[[package]]
name = "mdurl"
version = "0.1.2"
@@ -639,6 +880,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f1/a9/a10a10f12e50993b5a3568a1a90fd70b85f83edc451875d312bf60cd39b8/parsimonious-0.11.0-py3-none-any.whl", hash = "sha256:32e3818abf9f05b3b9f3b6d87d128645e30177e91f614d2277d88a0aea98fae2", size = 54351, upload-time = "2025-11-12T01:33:46.652Z" },
]
+[[package]]
+name = "parso"
+version = "0.8.7"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/30/4b/90c937815137d43ce71ba043cd3566221e9df6b9c805f24b5d138c9d40a7/parso-0.8.7.tar.gz", hash = "sha256:eaaac4c9fdd5e9e8852dc778d2d7405897ec510f2a298071453e5e3a07914bb1", size = 401824, upload-time = "2026-05-01T23:13:02.138Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl", hash = "sha256:a8926eb2a1b915486941fdbd31e86a4baf88fe8c210f25f2f35ecec5b574ca1c", size = 107025, upload-time = "2026-05-01T23:12:58.867Z" },
+]
+
[[package]]
name = "pathspec"
version = "1.1.1"
@@ -648,6 +898,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" },
]
+[[package]]
+name = "pexpect"
+version = "4.9.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "ptyprocess" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" },
+]
+
[[package]]
name = "pluggy"
version = "1.6.0"
@@ -657,6 +919,46 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]
+[[package]]
+name = "prompt-toolkit"
+version = "3.0.52"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "wcwidth" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" },
+]
+
+[[package]]
+name = "psutil"
+version = "7.2.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" },
+ { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" },
+ { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" },
+ { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" },
+ { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" },
+ { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" },
+ { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" },
+ { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" },
+]
+
[[package]]
name = "psycopg"
version = "3.3.4"
@@ -737,6 +1039,24 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/eb/e6/5fff07a70d1f945ed90ae131c3bd76cab32beff7c58c6db15ad5820b6d1f/psycopg_binary-3.3.4-cp314-cp314-win_amd64.whl", hash = "sha256:c37e024c07308cd06cf3ec51bfd0e7f6157585a4d84d1bce4a7f5f7913719bf8", size = 3666849, upload-time = "2026-05-01T23:31:51.165Z" },
]
+[[package]]
+name = "ptyprocess"
+version = "0.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" },
+]
+
+[[package]]
+name = "pure-eval"
+version = "0.2.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" },
+]
+
[[package]]
name = "pydantic"
version = "2.13.4"
@@ -906,6 +1226,14 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
]
+[[package]]
+name = "pytest-qgis"
+version = "4.0.1.post1"
+source = { git = "https://github.com/3liz/pytest-qgis.git#364633963a2d7b75aabde9044b8b79c3cd40c7ab" }
+dependencies = [
+ { name = "pytest" },
+]
+
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
@@ -1167,6 +1495,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/4e/b2/920464c907b191e37469d477a1aa8bc048b8f36c4c1610dfa4ab87b39e18/ruff-0.15.15-py3-none-win_arm64.whl", hash = "sha256:3c8ceca6792f38196b8f589bc92eccd03eef286602da92e5dc05cc42ef6441b7", size = 11138498, upload-time = "2026-05-28T14:16:38.425Z" },
]
+[[package]]
+name = "semver"
+version = "3.0.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/d1/d3159231aec234a59dd7d601e9dd9fe96f3afff15efd33c1070019b26132/semver-3.0.4.tar.gz", hash = "sha256:afc7d8c584a5ed0a11033af086e8af226a9c0b206f313e0301f8dd7b6b589602", size = 269730, upload-time = "2025-01-24T13:19:27.617Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a6/24/4d91e05817e92e3a61c8a21e08fd0f390f5301f1c448b137c57c4bc6e543/semver-3.0.4-py3-none-any.whl", hash = "sha256:9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746", size = 17912, upload-time = "2025-01-24T13:19:24.949Z" },
+]
+
[[package]]
name = "six"
version = "1.17.0"
@@ -1176,6 +1513,20 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
]
+[[package]]
+name = "stack-data"
+version = "0.6.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "asttokens" },
+ { name = "executing" },
+ { name = "pure-eval" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" },
+]
+
[[package]]
name = "stevedore"
version = "5.8.0"
@@ -1248,6 +1599,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl", hash = "sha256:15ccc861ac51c53696de0a5d6d4607f99c210739caf987b5d2054f3efed429d8", size = 58093, upload-time = "2025-10-17T04:03:20.435Z" },
]
+[[package]]
+name = "traitlets"
+version = "5.15.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1b/22/40f55b26baeab80c2d7b3f1db0682f8954e4617fee7d90ce634022ef05c6/traitlets-5.15.0.tar.gz", hash = "sha256:4fead733f81cf1c4c938e06f8ca4633896833c9d89eff878159457f4d4392971", size = 163197, upload-time = "2026-05-06T08:05:58.016Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/da/98/a9937a969d018a23badfea0b381f66783649d48e0ea6c41923265c3cbeb3/traitlets-5.15.0-py3-none-any.whl", hash = "sha256:fb36a18867a6803deab09f3c5e0fa81bb7b26a5c9e82501c9933f759166eff40", size = 85877, upload-time = "2026-05-06T08:05:55.853Z" },
+]
+
[[package]]
name = "transifex-python"
version = "3.7.0"
@@ -1311,6 +1671,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f5/be/f935130312330614811dae2ea9df3f395f6d63889eb6c2e68c14507152ee/vulture-2.16-py3-none-any.whl", hash = "sha256:6e0f1c312cef1c87856957e5c2ca9608834a7c794c2180477f30bf0e4cc58eee", size = 26993, upload-time = "2026-03-25T14:41:26.21Z" },
]
+[[package]]
+name = "wcwidth"
+version = "0.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/2c/ee/afaf0f85a9a18fe47a67f1e4422ed6cf1fe642f0ae0a2f81166231303c52/wcwidth-0.7.0.tar.gz", hash = "sha256:90e3a7ea092341c44b99562e75d09e4d5160fe7a3974c6fb842a101a95e7eed0", size = 182132, upload-time = "2026-05-02T16:04:12.653Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/41/52/e465037f5375f43533d1a80b6923955201596a99142ed524d77b571a1418/wcwidth-0.7.0-py3-none-any.whl", hash = "sha256:5d69154c429a82910e241c738cd0e2976fac8a2dd47a1a805f4afed1c0f136f2", size = 110825, upload-time = "2026-05-02T16:04:11.033Z" },
+]
+
[[package]]
name = "webdavclient3"
version = "3.14.7"