diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f74cf180..88677918 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -18,7 +18,7 @@ on:
jobs:
- flake8:
+ linter:
runs-on: ubuntu-latest
steps:
@@ -31,18 +31,11 @@ jobs:
- name: Checkout
uses: actions/checkout@v4.1.4
- - uses: actions/cache@v4.0.2
- with:
- path: ~/.cache/pip
- key: ${{ runner.os }}-pip-${{ hashFiles('requirements/dev.txt') }}
- restore-keys: |
- ${{ runner.os }}-pip-
-
- name: Install Python requirements
- run: pip install -r requirements/dev.txt
+ run: make install-tests
- - name: Run flake8
- run: flake8
+ - name: Run ruff
+ run: make lint
tests:
runs-on: ubuntu-latest
@@ -86,7 +79,7 @@ jobs:
./run-tests.sh
release:
- needs: [tests, flake8]
+ needs: [tests, linter]
runs-on: ubuntu-latest
if: github.repository_owner == '3liz' && contains(github.ref, 'refs/tags/')
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 2da91ef8..54a973cb 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -28,12 +28,12 @@ qgis-server:
tags:
- infrav3
-flake:
+linter:
+ image: ${REGISTRY_URL}/factory-ci-runner:qgis-ltr
stage: test
- before_script:
- - pip3 install --user -r requirements/dev.txt
script:
- - flake8
+ - source ~/.bashrc
+ - make install-tests lint FLAVOR=$QGIS_FLAVOR
tags:
- factory
diff --git a/Makefile b/Makefile
index 41ca03c8..c107db33 100644
--- a/Makefile
+++ b/Makefile
@@ -18,6 +18,9 @@ LOCAL_HOME ?= $(shell pwd)
SRCDIR=$(shell realpath .)
+PYTHON_PKG=lizmap_server
+TESTDIR=test
+
tests:
@mkdir -p $$(pwd)/.local $(LOCAL_HOME)/.cache
@echo Do not forget to run docker pull $(QGIS_IMAGE) from time to time
@@ -30,3 +33,21 @@ tests:
-e QGIS_SERVER_LIZMAP_REVEAL_SETTINGS=TRUE \
-e PYTEST_ADDOPTS="$(TEST_OPTS) --assert=plain" \
$(QGIS_IMAGE) ./run-tests.sh
+
+.PHONY: test
+
+install-tests:
+ pip install -U --upgrade-strategy=eager -r requirements/dev.txt
+
+export QGIS_SERVER_LIZMAP_REVEAL_SETTINGS=TRUE
+test:
+ cd test && pytest -v --qgis-plugins=..
+
+lint:
+ @ruff check $(PYTHON_PKG) $(TESTDIR)
+
+lint-preview:
+ @ruff check --preview $(PYTHON_PKG) $(TESTDIR)
+
+lint-fix:
+ @ruff check --fix --preview $(PYTHON_PKG) $(TESTDIR)
diff --git a/lizmap_server/core.py b/lizmap_server/core.py
index f7234576..b05d3d31 100755
--- a/lizmap_server/core.py
+++ b/lizmap_server/core.py
@@ -333,7 +333,7 @@ def is_editing_context(handler: QgsRequestHandler) -> bool:
return False
-def server_feature_id_expression(feature_id, data_provider: QgsVectorDataProvider) -> str:
+def server_feature_id_expression(feature_id: str, data_provider: QgsVectorDataProvider) -> str:
""" Fetch the QGIS server feature ID expression according to the current QGIS version. """
if Qgis.QGIS_VERSION_INT >= 32400:
from qgis.server import QgsServerFeatureId
@@ -344,7 +344,7 @@ def server_feature_id_expression(feature_id, data_provider: QgsVectorDataProvide
return _server_feature_id_expression(feature_id, data_provider.pkAttributeIndexes(), data_provider.fields())
-def _server_feature_id_expression(feature_id, pk_attributes: list, fields: QgsFields) -> str:
+def _server_feature_id_expression(feature_id: str, pk_attributes: list, fields: QgsFields) -> str:
""" Port of QgsServerFeatureId::getExpressionFromServerFid for QGIS < 3.24
The value "@@" is hardcoded in the CPP file.
diff --git a/lizmap_server/expression_service.py b/lizmap_server/expression_service.py
index fb28a834..c6599852 100755
--- a/lizmap_server/expression_service.py
+++ b/lizmap_server/expression_service.py
@@ -221,7 +221,7 @@ def evaluate(params: Dict[str, str], response: QgsServerResponse, project: QgsPr
'status': 'success',
'results': [],
'errors': [],
- 'features': 0
+ 'features': 0,
}
# without features just evaluate expression with layer context
@@ -421,7 +421,7 @@ def replace_expression_text(
'status': 'success',
'results': [],
'errors': [],
- 'features': 0
+ 'features': 0,
}
# without features just replace expression string with layer context
@@ -536,8 +536,8 @@ def replace_expression_text(
response.write(
',\n'.join([
'{"type": "FeatureCollection"',
- '"features": [' + ',\n'.join(body['results']) + ']}'
- ])
+ '"features": [' + ',\n'.join(body['results']) + ']}',
+ ]),
)
else:
write_json_response(body, response)
diff --git a/lizmap_server/filter_by_polygon.py b/lizmap_server/filter_by_polygon.py
index 05d96123..10af5cdb 100755
--- a/lizmap_server/filter_by_polygon.py
+++ b/lizmap_server/filter_by_polygon.py
@@ -151,7 +151,7 @@ def is_valid(self) -> bool:
return True
- def sql_query(self, uri: QgsDataSourceUri, sql) -> Tuple[Tuple]:
+ def sql_query(self, uri: QgsDataSourceUri, sql: str) -> Tuple[Tuple]:
""" For a given URI, execute an SQL query and return the result. """
if self.connection is None:
# noinspection PyArgumentList
@@ -205,7 +205,7 @@ def subset_sql(self, groups_or_user: tuple) -> Tuple[str, str]:
ewkt = "SRID={crs};{wkt}".format(
crs=self.polygon.crs().postgisSrid(),
- wkt=polygon.asWkt(6 if self.polygon.crs().isGeographic() else 2)
+ wkt=polygon.asWkt(6 if self.polygon.crs().isGeographic() else 2),
)
use_st_intersect = False if self.spatial_relationship == 'contains' else True
@@ -266,7 +266,7 @@ def _polygon_for_groups_with_qgis_api(self, groups_or_user: tuple) -> QgsGeometr
)
)""".format(
polygon_field=self.group_field,
- groups_or_user=','.join(groups_or_user)
+ groups_or_user=','.join(groups_or_user),
)
# Create request
@@ -492,13 +492,13 @@ def _format_qgis_expression_relationship(
:returns: The QGIS expression
"""
geom = "geom_from_wkt('{wkt}')".format(
- wkt=polygons.asWkt(6 if filtering_crs.isGeographic() else 2)
+ wkt=polygons.asWkt(6 if filtering_crs.isGeographic() else 2),
)
if filtering_crs != filtered_crs:
geom = "transform({geom}, '{from_crs}', '{to_crs}')".format(
geom=geom,
from_crs=filtering_crs.authid(),
- to_crs=filtered_crs.authid()
+ to_crs=filtered_crs.authid(),
)
if use_centroid:
diff --git a/lizmap_server/get_feature_info.py b/lizmap_server/get_feature_info.py
index df49964f..8d81c190 100755
--- a/lizmap_server/get_feature_info.py
+++ b/lizmap_server/get_feature_info.py
@@ -19,6 +19,7 @@
QgsFeature,
QgsFeatureRequest,
QgsProject,
+ QgsRelationManager,
)
from qgis.server import QgsServerFilter, QgsServerProjectUtils
@@ -78,7 +79,8 @@ def append_maptip(cls, string: str, layer_name: str, feature_id: Union[str, int]
return xml_string.strip()
@classmethod
- def feature_list_to_replace(cls, cfg: dict, project: QgsProject, relation_manager, xml) -> List[Result]:
+ def feature_list_to_replace(
+ cls, cfg: dict, project: QgsProject, relation_manager: QgsRelationManager, xml: str) -> List[Result]:
""" Parse the XML and check for each layer according to the Lizmap CFG file. """
features = []
for layer_name, feature_id in GetFeatureInfoFilter.parse_xml(xml):
@@ -191,13 +193,13 @@ def responseComplete(self):
# The user has clicked in a random area on the map or no interesting LAYERS,
# no features are returned.
logger.info(
- "No features found in the XML from QGIS Server for project {}".format(project_path)
+ "No features found in the XML from QGIS Server for project {}".format(project_path),
)
return
logger.info(
"Replacing the maptip from QGIS by the drag and drop expression for {} features on {}".format(
- len(features), ','.join([result.layer.name() for result in features]))
+ len(features), ','.join([result.layer.name() for result in features])),
)
# Let's evaluate each expression popup
@@ -231,7 +233,7 @@ def responseComplete(self):
logger.warning(
"The feature {} for layer {} is not valid, skip replacing this XML "
"GetFeatureInfo, continue to the next feature".format(
- result.feature_id, result.layer.id())
+ result.feature_id, result.layer.id()),
)
continue
@@ -243,7 +245,7 @@ def responseComplete(self):
logger.warning(
"The GetFeatureInfo result for feature {} in layer {} is not valid, skip replacing "
"this XML GetFeatureInfo, , continue to the next feature".format(
- result.feature_id, result.layer.id())
+ result.feature_id, result.layer.id()),
)
continue
diff --git a/lizmap_server/get_legend_graphic.py b/lizmap_server/get_legend_graphic.py
index f870893d..750016a9 100644
--- a/lizmap_server/get_legend_graphic.py
+++ b/lizmap_server/get_legend_graphic.py
@@ -100,7 +100,7 @@ def responseComplete(self):
'title': layer_name,
'icon': self.warning_icon(),
'valid': False,
- }]
+ }],
}
handler.clearBody()
handler.appendBody(json.dumps(json_data).encode('utf8'))
@@ -183,7 +183,7 @@ def responseComplete(self):
@classmethod
def _extract_categories(
- cls, layer: QgsVectorLayer, show_feature_count: bool = False, project_path: str = ""
+ cls, layer: QgsVectorLayer, show_feature_count: bool = False, project_path: str = "",
) -> dict:
""" Extract categories from the layer legend. """
# TODO Annotations QGIS 3.22 [str, Category]
@@ -209,14 +209,14 @@ def _extract_categories(
if not result:
Logger.warning(
f"The expression in the project '{project_path}', layer '{layer.name()}' has not "
- f"been generated correctly, setting the expression to an empty string"
+ f"been generated correctly, setting the expression to an empty string",
)
expression = ''
if item.label() in categories.keys():
Logger.warning(
f"The label key '{item.label()}' is not unique, expect the legend to be broken in the project "
- f"'{project_path}', layer '{layer.name()}'."
+ f"'{project_path}', layer '{layer.name()}'.",
)
categories[item.label()] = Category(
diff --git a/lizmap_server/lizmap_accesscontrol.py b/lizmap_server/lizmap_accesscontrol.py
index d9010a6e..bd254b2d 100755
--- a/lizmap_server/lizmap_accesscontrol.py
+++ b/lizmap_server/lizmap_accesscontrol.py
@@ -116,14 +116,14 @@ def layerPermissions(self, layer: QgsMapLayer) -> QgsAccessControlFilter.LayerPe
# Check lizmap edition config
layer_id = layer.id()
- if 'editionLayers' in cfg and cfg['editionLayers']:
+ if cfg.get('editionLayers'):
if layer_id in cfg['editionLayers'] and cfg['editionLayers'][layer_id]:
edit_layer = cfg['editionLayers'][layer_id]
# Check if edition is possible
# By default not
can_edit = False
- if 'acl' in edit_layer and edit_layer['acl']:
+ if edit_layer.get('acl'):
# acl is defined and not an empty string
# authorization defined for edition
group_edit = edit_layer['acl'].split(',')
@@ -383,7 +383,7 @@ def _filter_by_login(cfg_layer_login_filter: dict, groups: tuple, login: str) ->
# Build filter
layer_filter = '{} IN ({})'.format(
QgsExpression.quotedColumnRef(cfg_layer_login_filter['filterAttribute']),
- ', '.join(quoted_values)
+ ', '.join(quoted_values),
)
return layer_filter
diff --git a/lizmap_server/lizmap_service.py b/lizmap_server/lizmap_service.py
index 33ab1d4b..8e61f238 100755
--- a/lizmap_server/lizmap_service.py
+++ b/lizmap_server/lizmap_service.py
@@ -112,7 +112,7 @@ def polygon_filter(
body = {
'status': 'success',
'filter': ALL_FEATURES,
- 'polygons': ''
+ 'polygons': '',
}
# Check first the headers to avoid unnecessary config file reading
@@ -166,7 +166,7 @@ def polygon_filter(
body = {
'status': 'success',
'filter': NO_FEATURES,
- 'polygons': ''
+ 'polygons': '',
}
write_json_response(body, response)
return
@@ -199,7 +199,7 @@ def polygon_filter(
body = {
'status': 'success',
'filter': NO_FEATURES,
- 'polygons': ''
+ 'polygons': '',
}
write_json_response(body, response)
diff --git a/lizmap_server/logger.py b/lizmap_server/logger.py
index 4a997dbc..b42d9fb8 100755
--- a/lizmap_server/logger.py
+++ b/lizmap_server/logger.py
@@ -38,8 +38,8 @@ def log_exception(e: BaseException):
Logger.critical(
"Critical exception:\n{e}\n{traceback}".format(
e=e,
- traceback=traceback.format_exc()
- )
+ traceback=traceback.format_exc(),
+ ),
)
diff --git a/lizmap_server/server_info_handler.py b/lizmap_server/server_info_handler.py
index 0aaa904d..6865e179 100755
--- a/lizmap_server/server_info_handler.py
+++ b/lizmap_server/server_info_handler.py
@@ -62,7 +62,7 @@ def plugins_installed(py_qgis_server: bool) -> list:
return server_active_plugins
-def plugin_metadata_key(py_qgis_server: bool, name: str, key: str, ) -> str:
+def plugin_metadata_key(py_qgis_server: bool, name: str, key: str) -> str:
""" Return the version for a given plugin. """
unknown = 'unknown'
# it seems configparser is transforming all keys as lowercase...
@@ -89,8 +89,8 @@ def plugin_metadata_key(py_qgis_server: bool, name: str, key: str, ) -> str:
('version', str),
('build_id', Union[int, None]),
('commit_id', Union[int, None]),
- ('is_stable', bool)
- ]
+ ('is_stable', bool),
+ ],
)
@@ -217,7 +217,7 @@ def handleRequest(self, context):
'gdal': gdal.VersionInfo('VERSION_NUM'),
'python': sys.version,
'qt': Qt.QT_VERSION_STR,
- }
+ },
}
self.write(data, context)
@@ -244,6 +244,6 @@ def parameters(self, context):
"CHECK_CUSTOM_HEADERS",
False,
QgsServerQueryStringParameter.Type.String,
- "If we check custom headers"
+ "If we check custom headers",
),
]
diff --git a/lizmap_server/tools.py b/lizmap_server/tools.py
index 30efcd4c..5a647415 100755
--- a/lizmap_server/tools.py
+++ b/lizmap_server/tools.py
@@ -56,7 +56,7 @@ def check_environment_variable() -> bool:
'https://docs.lizmap.com/current/en/install/pre_requirements.html#lizmap-server-plugin '
'An environment variable must be enabled to have Lizmap Web Client ≥ 3.5 working.',
"Lizmap",
- Qgis.Critical
+ Qgis.Critical,
)
return False
diff --git a/lizmap_server/tooltip.py b/lizmap_server/tooltip.py
index ade778a6..ffb0339f 100755
--- a/lizmap_server/tooltip.py
+++ b/lizmap_server/tooltip.py
@@ -82,7 +82,7 @@ def create_popup_node_item_from_form(
alias = field.alias()
name = field.name()
fname = alias if alias else name
- fname = fname.replace("'", "’")
+ fname = fname.replace("'", "’") # noqa RUF001
# adapt the view depending on the field type
field_widget_setup = field.editorWidgetSetup()
@@ -145,9 +145,9 @@ def create_popup_node_item_from_form(
if node.visibilityExpression().enabled():
visibility = Tooltip._generate_eval_visibility(node.visibilityExpression().data().expression())
- l = level
+ lvl = level
# create div container
- if l == 1:
+ if lvl == 1:
active = ''
if not headers:
active = 'active'
@@ -159,14 +159,16 @@ def create_popup_node_item_from_form(
active = '{} {}'.format(active, visibility)
if visibility and not active:
active = visibility
- h += '\n' + SPACES + '
{}'.format(
- active, regex.sub('_', node.name()), node.name())
+ h += '\n' + SPACES
+ h += (
+ '{}'
+ ).format(active, regex.sub('_', node.name()), node.name())
headers.append(h)
- if l > 1:
- a += '\n' + SPACES * l + ''
html += a
return html
@staticmethod
- def _generate_field_view(name: str):
- return '"{}"'.format(name)
+ def _generate_field_view(name: str) -> str:
+ return f'"{name}"'
@staticmethod
- def _generate_eval_visibility(expression: str):
- return "[% if ({}, '', 'hidden') %]".format(expression)
+ def _generate_eval_visibility(expression: str) -> str:
+ return f"[% if ({expression}, '', 'hidden') %]"
@staticmethod
def _generate_attribute_editor_relation(label: str, relation_id: str, referencing_layer_id: str) -> str:
@@ -235,7 +237,12 @@ def _generate_attribute_editor_relation(label: str, relation_id: str, referencin
return result
@staticmethod
- def _generate_relation_reference(name: str, parent_pk: str, layer_id: str, display_expression: str):
+ def _generate_relation_reference(
+ name: str,
+ parent_pk: str,
+ layer_id: str,
+ display_expression: str,
+ ) -> str:
expression = '''
"{}" = attribute(@parent, '{}')
'''.format(parent_pk, name)
@@ -249,12 +256,12 @@ def _generate_relation_reference(name: str, parent_pk: str, layer_id: str, displ
)'''.format(
layer_id,
display_expression,
- expression
+ expression,
)
return field_view
@staticmethod
- def _generate_field_name(name: str, fname: str, expression: str):
+ def _generate_field_name(name: str, fname: str, expression: str) -> str:
text = '''
[% CASE
WHEN "{0}" IS NOT NULL OR trim("{0}") != ''
@@ -267,12 +274,12 @@ def _generate_field_name(name: str, fname: str, expression: str):
END %]'''.format(
name,
fname,
- expression
+ expression,
)
return text
@staticmethod
- def _generate_value_map(widget_config: Union[list, dict], name: str):
+ def _generate_value_map(widget_config: Union[list, dict], name: str) -> str:
def escape_value(value: str) -> str:
"""Change ' to ’ for the HStore function. """
return value.replace("'", "’")
@@ -305,7 +312,7 @@ def escape_value(value: str) -> str:
return field_view
@staticmethod
- def _generate_external_resource(widget_config: dict, name: str, fname: str):
+ def _generate_external_resource(widget_config: dict, name: str, fname: str) -> str:
dview = widget_config['DocumentViewer']
if dview == QgsExternalResourceWidget.Image:
@@ -354,7 +361,7 @@ def _generate_external_resource(widget_config: dict, name: str, fname: str):
return field_view
@staticmethod
- def _generate_date(widget_config: dict, name: str):
+ def _generate_date(widget_config: dict, name: str) -> str:
date_format = widget_config.get('display_format')
if not date_format:
@@ -369,12 +376,12 @@ def _generate_date(widget_config: dict, name: str):
return field_view
@staticmethod
- def _generate_value_relation(widget_config: dict, name: str):
+ def _generate_value_relation(widget_config: dict, name: str) -> str:
vlid = widget_config['Layer']
expression = '''"{}" = attribute(@parent, '{}')'''.format(
widget_config['Key'],
- name
+ name,
)
filter_exp = widget_config.get('FilterExpression', '').strip()
@@ -394,19 +401,19 @@ def _generate_value_relation(widget_config: dict, name: str):
)'''.format(
vlid,
widget_config['Value'],
- expression
+ expression,
)
return field_view
@staticmethod
- def _generate_text_label(label: str, expression: str):
+ def _generate_text_label(label: str, expression: str) -> str:
text = '''
{0}
{1}
'''.format(
label,
- expression
+ expression,
)
return text
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 00000000..7d84133b
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,35 @@
+# Ruff configuration
+# See https://doc.astral.sh/ruff/configuration
+
+[tool.ruff]
+line-length = 120
+target-version = "py310"
+exclude = [
+ ".venv",
+ ".local",
+ ".test/.local",
+]
+
+[tool.ruff.format]
+indent-style = "space"
+
+[tool.ruff.lint]
+extend-select = ["E", "F", "ANN", "W", "T", "COM", "RUF"]
+ignore = ["ANN101", "ANN102", "ANN002", "ANN003", "RUF100"]
+
+[tool.ruff.lint.per-file-ignores]
+"tests/*" = ["T201", "E501"]
+"test/*" = ["T201", "E501"]
+"lizmap_server/tooltip.py" = ["RUF001", "RUF002"]
+
+
+[tool.ruff.lint.isort]
+lines-between-types = 1
+known-third-party = [
+ "qgis",
+]
+
+[tool.ruff.lint.flake8-annotations]
+ignore-fully-untyped = true
+suppress-none-returning = true
+suppress-dummy-args = true
diff --git a/requirements/dev.txt b/requirements/dev.txt
index ee214acd..863b4337 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -1,15 +1,6 @@
-flake8
-flake8-absolute-import
-# flake8-bugbear
-flake8-builtins
-flake8-isort
-# flake8-multiline-containers
-# flake8-mutable
-# flake8-pep3101
-flake8-print
-# flake8-variables-names
-isort
-pylint
-# pycodestyle
-# PyQt5
-# PyQt5-stubs
+pytest
+ruff
+mypy
+mypy-extensions
+pipdeptree
+xmldiff
diff --git a/setup.cfg b/setup.cfg
index c0e1ab07..efb2e711 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,44 +1,44 @@
-[flake8]
-max-line-length = 130
-ignore =
- # Closing bracket does not match visual indentation
- E123,
- E124,
- E125,
- E126,
- # Line length
- E501,
- # Bad double quotes
- Q000,
- Q001,
- Q003,
- # Line break before binary operator
- W503,
-
-per-file-ignores =
- lizmap_server/tooltip.py:E741
- lizmap_server/lizmap_service.py:ABS101
- lizmap_server/lizmap_server.py:ABS101
- lizmap_server/expression_service.py:ABS101
- lizmap_server/lizmap_accesscontrol.py:ABS101
-
-exclude =
- test/conftest.py,
- .venv/,
- .local/,
- .local/lib,
- ./test/.local/lib
-
-[isort]
-multi_line_output = 3
-include_trailing_comma = True
-use_parentheses = True
-ensure_newline_before_comments = True
-lines_between_types = 1
-skip =
- .venv,
- .local/,
- .cache/,
+; [flake8]
+; max-line-length = 130
+; ignore =
+; # Closing bracket does not match visual indentation
+; E123,
+; E124,
+; E125,
+; E126,
+; # Line length
+; E501,
+; # Bad double quotes
+; Q000,
+; Q001,
+; Q003,
+; # Line break before binary operator
+; W503,
+;
+; per-file-ignores =
+; lizmap_server/tooltip.py:E741
+; lizmap_server/lizmap_service.py:ABS101
+; lizmap_server/lizmap_server.py:ABS101
+; lizmap_server/expression_service.py:ABS101
+; lizmap_server/lizmap_accesscontrol.py:ABS101
+;
+; exclude =
+; test/conftest.py,
+; .venv/,
+; .local/,
+; .local/lib,
+; ./test/.local/lib
+;
+; [isort]
+; multi_line_output = 3
+; include_trailing_comma = True
+; use_parentheses = True
+; ensure_newline_before_comments = True
+; lines_between_types = 1
+; skip =
+; .venv,
+; .local/,
+; .cache/,
[qgis-plugin-ci]
plugin_path = lizmap_server
diff --git a/test/conftest.py b/test/conftest.py
index deb0546a..5ce62555 100644
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -10,28 +10,31 @@
from qgis.PyQt import Qt
+from typing import Any, Dict, Generator, Optional
+
+from qgis.core import Qgis, QgsApplication, QgsFontUtils, QgsProject
+from qgis.server import (
+ QgsBufferServerRequest,
+ QgsBufferServerResponse,
+ QgsServer,
+ QgsServerRequest,
+ QgsServerInterface,
+)
+
with warnings.catch_warnings():
warnings.filterwarnings("ignore", category=DeprecationWarning)
from osgeo import gdal
-logging.basicConfig( stream=sys.stderr )
+logging.basicConfig(stream=sys.stderr)
logging.disable(logging.NOTSET)
LOGGER = logging.getLogger('server')
LOGGER.setLevel(logging.DEBUG)
-from typing import Any, Dict, Generator
-
-from qgis.core import Qgis, QgsApplication, QgsFontUtils, QgsProject
-from qgis.server import (
- QgsBufferServerRequest,
- QgsBufferServerResponse,
- QgsServer,
- QgsServerRequest,
-)
qgis_application = None
+
def pytest_addoption(parser):
parser.addoption("--qgis-plugins", metavar="PATH", help="Plugin path", default=None)
@@ -60,8 +63,8 @@ def pytest_sessionstart(session):
os.environ['QT_QPA_PLATFORM'] = 'offscreen'
# Define this in global environment
- #os.environ['QGIS_DISABLE_MESSAGE_HOOKS'] = 1
- #os.environ['QGIS_NO_OVERRIDE_IMPORT'] = 1
+ # os.environ['QGIS_DISABLE_MESSAGE_HOOKS'] = 1
+ # os.environ['QGIS_NO_OVERRIDE_IMPORT'] = 1
qgis_application = QgsApplication([], False)
qgis_application.initQgis()
@@ -86,9 +89,10 @@ def pytest_sessionstart(session):
'wcs': "http://www.opengis.net/wcs",
'ows': "http://www.opengis.net/ows/1.1",
'gml': "http://www.opengis.net/gml",
- 'xsi': "http://www.w3.org/2001/XMLSchema-instance"
+ 'xsi': "http://www.w3.org/2001/XMLSchema-instance",
}
+
class OWSResponse:
def __init__(self, resp: QgsBufferServerResponse) -> None:
@@ -128,7 +132,7 @@ def client(request):
"""
class _Client:
- def __init__(self) -> None:
+ def __init__(self):
self.fontFamily = QgsFontUtils.standardTestFontFamily()
QgsFontUtils.loadStandardTestFonts(['All'])
@@ -138,12 +142,11 @@ def __init__(self) -> None:
self.datapath = request.config.rootdir.join('data')
self.server = QgsServer()
-
# Load plugins
load_plugins(self.server.serverInterface())
- def getplugin(self, name) -> Any:
- """ retourne l'instance du plugin
+ def getplugin(self, name: str) -> Any: # noqa ANN401
+ """ Return the instance of the plugin
"""
return server_plugins.get(name)
@@ -160,7 +163,7 @@ def get_project(self, name: str) -> QgsProject:
raise ValueError("Error reading project '%s':" % projectpath.strpath)
return qgsproject
- def get(self, query: str, project: str = None, headers: Dict[str, str] = None) -> OWSResponse:
+ def get(self, query: str, project: Optional[str] = None, headers: Optional[Dict[str, str]] = None) -> OWSResponse:
""" Return server response from query
"""
if headers is None:
@@ -174,7 +177,7 @@ def get(self, query: str, project: str = None, headers: Dict[str, str] = None) -
self.server.handleRequest(server_request, response, project=qgsproject)
return OWSResponse(response)
- def get_with_project(self, query: str, project: QgsProject, headers: Dict[str, str] = None) -> OWSResponse:
+ def get_with_project(self, query: str, project: QgsProject, headers: Optional[Dict[str, str]] = None) -> OWSResponse:
""" Return server response from query
"""
if headers is None:
@@ -189,7 +192,7 @@ def get_with_project(self, query: str, project: QgsProject, headers: Dict[str, s
##
-## Plugins
+# Plugins
##
def checkQgisVersion(minver: str, maxver: str) -> bool:
@@ -198,22 +201,21 @@ def to_int(ver):
major, *ver = ver.split('.')
major = int(major)
minor = int(ver[0]) if len(ver) > 0 else 0
- rev = int(ver[1]) if len(ver) > 1 else 0
+ rev = int(ver[1]) if len(ver) > 1 else 0
if minor >= 99:
minor = rev = 0
major += 1
if rev > 99:
rev = 99
- return int("{:d}{:02d}{:02d}".format(major,minor,rev))
-
+ return int("{:d}{:02d}{:02d}".format(major, minor, rev))
version = to_int(Qgis.QGIS_VERSION.split('-')[0])
- minver = to_int(minver) if minver else version
- maxver = to_int(maxver) if maxver else version
+ minver = to_int(minver) if minver else version
+ maxver = to_int(maxver) if maxver else version
return minver <= version <= maxver
-def find_plugins(pluginpath: str) -> Generator[str,None,None]:
+def find_plugins(pluginpath: str) -> Generator[str, None, None]:
""" Load plugins
"""
for plugin in glob.glob(os.path.join(plugin_path + "/*")):
@@ -235,25 +237,25 @@ def find_plugins(pluginpath: str) -> Generator[str,None,None]:
minver = cp['general'].get('qgisMinimumVersion')
maxver = cp['general'].get('qgisMaximumVersion')
except Exception as exc:
- LOGGER.critical("Error reading plugin metadata '%s': %s",metadatafile,exc)
+ LOGGER.critical("Error reading plugin metadata '%s': %s", metadatafile, exc)
continue
- if not checkQgisVersion(minver,maxver):
+ if not checkQgisVersion(minver, maxver):
LOGGER.critical(("Unsupported version for %s:"
"\n MinimumVersion: %s"
"\n MaximumVersion: %s"
"\n Qgis version: %s"
- "\n Discarding") % (plugin,minver,maxver,
+ "\n Discarding") % (plugin, minver, maxver,
Qgis.QGIS_VERSION.split('-')[0]))
continue
yield os.path.basename(plugin)
-server_plugins = {}
+server_plugins: Dict[str, Any] = {}
-def load_plugins(serverIface: 'QgsServerInterface') -> None:
+def load_plugins(serverIface: QgsServerInterface) -> None:
""" Start all plugins
"""
if not plugin_path:
@@ -270,9 +272,9 @@ def load_plugins(serverIface: 'QgsServerInterface') -> None:
# Initialize the plugin
server_plugins[plugin] = package.serverClassFactory(serverIface)
- LOGGER.info("Loaded plugin %s",plugin)
+ LOGGER.info("Loaded plugin %s", plugin)
except:
- LOGGER.error("Error loading plugin %s",plugin)
+ LOGGER.error("Error loading plugin %s", plugin)
raise
@@ -280,14 +282,14 @@ def load_plugins(serverIface: 'QgsServerInterface') -> None:
# Logger hook
#
-def install_logger_hook( verbose: bool=False ) -> None:
+def install_logger_hook(verbose: bool = False) -> None:
""" Install message log hook
"""
- from qgis.core import Qgis, QgsApplication, QgsMessageLog
+ from qgis.core import Qgis, QgsApplication
# Add a hook to qgis message log
def writelogmessage(message, tag, level):
- arg = '{}: {}'.format( tag, message )
+ arg = '{}: {}'.format(tag, message)
if level == Qgis.Warning:
LOGGER.warning(arg)
elif level == Qgis.Critical:
@@ -298,4 +300,4 @@ def writelogmessage(message, tag, level):
LOGGER.info(arg)
messageLog = QgsApplication.messageLog()
- messageLog.messageReceived.connect( writelogmessage )
+ messageLog.messageReceived.connect(writelogmessage)
diff --git a/test/test_expression_service_getfeaturewithformscope.py b/test/test_expression_service_getfeaturewithformscope.py
index 47897631..d2493170 100644
--- a/test/test_expression_service_getfeaturewithformscope.py
+++ b/test/test_expression_service_getfeaturewithformscope.py
@@ -55,9 +55,9 @@ def test_comment_space_carriage_return(client):
"type": "Feature",
"geometry": {
"type": "Point",
- "coordinates": [102.0, 0.5]
+ "coordinates": [102.0, 0.5],
},
- "properties": {"polygon_id": 4}})
+ "properties": {"polygon_id": 4}}),
}
rv = client.get(_build_query_string(qs), project_file)
b = _check_request(rv)
@@ -114,8 +114,8 @@ def test_request_get_feature_form_scope_current_value(client):
'FORM_FEATURE': json.dumps(
{
"type": "Feature", "geometry": {"type": "Point", "coordinates": [102.0, 0.5]},
- "properties": {"prop0": "Bretagne"}
- }
+ "properties": {"prop0": "Bretagne"},
+ },
)}
rv = client.get(_build_query_string(qs), PROJECT_FILE)
b = _check_request(rv)
@@ -153,8 +153,8 @@ def test_request_get_feature_form_scope_with_geom(client):
'FORM_FEATURE': json.dumps(
{
"type": "Feature", "geometry": {"type": "Point", "coordinates": [102.0, 0.5]},
- "properties": {"prop0": "Bretagne"}
- }
+ "properties": {"prop0": "Bretagne"},
+ },
),
'WITH_GEOMETRY': 'True'}
rv = client.get(_build_query_string(qs), PROJECT_FILE)
@@ -192,8 +192,8 @@ def test_request_get_feature_form_scope_with_fields(client):
'FORM_FEATURE': json.dumps(
{
"type": "Feature", "geometry": {"type": "Point", "coordinates": [102.0, 0.5]},
- "properties": {"prop0": "Bretagne"}
- }
+ "properties": {"prop0": "Bretagne"},
+ },
),
'FIELDS': 'ISO,NAME_1'}
rv = client.get(_build_query_string(qs), PROJECT_FILE)
@@ -230,8 +230,8 @@ def test_request_get_feature_form_scope_with_spatial_filter(client):
'FORM_FEATURE': json.dumps(
{
"type": "Feature", "geometry": {"type": "Point", "coordinates": [-3.0, 48.0]},
- "properties": {"prop0": "Bretagne"}
- }
+ "properties": {"prop0": "Bretagne"},
+ },
)}
rv = client.get(_build_query_string(qs), PROJECT_FILE)
b = _check_request(rv)
@@ -269,8 +269,8 @@ def test_request_get_feature_without_named_parameters(client):
'FORM_FEATURE': json.dumps(
{
"type": "Feature", "geometry": {"type": "Point", "coordinates": [102.0, 0.5]},
- "properties": {"polygon_id": 4}
- }
+ "properties": {"polygon_id": 4},
+ },
)}
rv = client.get(_build_query_string(qs), project_file)
b = _check_request(rv)
@@ -295,8 +295,8 @@ def test_request_get_feature_with_named_parameters(client):
'FORM_FEATURE': json.dumps(
{
"type": "Feature", "geometry": {"type": "Point", "coordinates": [102.0, 0.5]},
- "properties": {"polygon_id": 4}
- }
+ "properties": {"polygon_id": 4},
+ },
)}
rv = client.get(_build_query_string(qs), project_file)
b = _check_request(rv)
@@ -319,14 +319,14 @@ def test_request_given_parent_feature(client):
'FORM_FEATURE': json.dumps(
{
"type": "Feature", "geometry": {"type": "Point", "coordinates": [-3.0, 48.0]},
- "properties": {"prop1": "Rennes"}
- }
+ "properties": {"prop1": "Rennes"},
+ },
),
'PARENT_FEATURE': json.dumps(
{
"type": "Feature", "geometry": {"type": "Point", "coordinates": [102.0, 0.5]},
- "properties": {"prop0": "Bretagne"}
- }
+ "properties": {"prop0": "Bretagne"},
+ },
)}
rv = client.get(_build_query_string(qs), PROJECT_FILE)
b = _check_request(rv)
@@ -364,14 +364,14 @@ def test_request_current_parent_feature(client):
'FORM_FEATURE': json.dumps(
{
"type": "Feature", "geometry": {"type": "Point", "coordinates": [-3.0, 48.0]},
- "properties": {"prop1": "Rennes"}
- }
+ "properties": {"prop1": "Rennes"},
+ },
),
'PARENT_FEATURE': json.dumps(
{
"type": "Feature", "geometry": {"type": "Point", "coordinates": [102.0, 0.5]},
- "properties": {"prop0": "Bretagne"}
- }
+ "properties": {"prop0": "Bretagne"},
+ },
)}
rv = client.get(_build_query_string(qs), PROJECT_FILE)
b = _check_request(rv)
@@ -409,14 +409,14 @@ def test_request_current_parent_geometry(client):
'FORM_FEATURE': json.dumps(
{
"type": "Feature", "geometry": {"type": "Point", "coordinates": [102.0, 0.5]},
- "properties": {"prop1": "Rennes"}
- }
+ "properties": {"prop1": "Rennes"},
+ },
),
'PARENT_FEATURE': json.dumps(
{
"type": "Feature", "geometry": {"type": "Point", "coordinates": [-3.0, 48.0]},
- "properties": {"prop0": "Bretagne"}
- }
+ "properties": {"prop0": "Bretagne"},
+ },
)}
rv = client.get(_build_query_string(qs), PROJECT_FILE)
b = _check_request(rv)
diff --git a/test/test_filter_by_polygon.py b/test/test_filter_by_polygon.py
index 7bb9c998..78cdc91b 100644
--- a/test/test_filter_by_polygon.py
+++ b/test/test_filter_by_polygon.py
@@ -1,7 +1,3 @@
-__copyright__ = 'Copyright 2021, 3Liz'
-__license__ = 'GPL version 3'
-__email__ = 'info@3liz.org'
-
""" Test filter by polygon. """
import unittest
@@ -17,6 +13,9 @@
from lizmap_server.filter_by_polygon import FilterByPolygon
+__copyright__ = 'Copyright 2021, 3Liz'
+__license__ = 'GPL version 3'
+__email__ = 'info@3liz.org'
class TestFilterByPolygon(unittest.TestCase):
@@ -25,16 +24,16 @@ def test_not_filtered_layer(self):
json = {
"config": {
"polygon_layer_id": "FOO",
- "group_field": "groups"
+ "group_field": "groups",
},
"layers": [
{
"layer": "BAR",
"primary_key": "primary",
"spatial_relationship": "intersects",
- "filter_mode": "display_and_editing"
- }
- ]
+ "filter_mode": "display_and_editing",
+ },
+ ],
}
points = QgsVectorLayer('Point?field=id:integer', 'points', 'memory')
self.assertFalse(FilterByPolygon(json, points).is_filtered())
@@ -86,16 +85,16 @@ def test_filter_by_polygon_filter(self):
json = {
"config": {
"polygon_layer_id": polygon.id(),
- "group_field": "groups"
+ "group_field": "groups",
},
"layers": [
{
"layer": points.id(),
"primary_key": "id",
"spatial_relationship": "intersects",
- "filter_mode": "display_and_editing"
- }
- ]
+ "filter_mode": "display_and_editing",
+ },
+ ],
}
project = QgsProject.instance()
@@ -150,16 +149,16 @@ def test_filter_by_polygon_filter(self):
json = {
"config": {
"polygon_layer_id": polygon.id(),
- "group_field": "groups"
+ "group_field": "groups",
},
"layers": [
{
"layer": points.id(),
"primary_key": "id",
"spatial_relationship": "intersects",
- "filter_mode": "editing"
- }
- ]
+ "filter_mode": "editing",
+ },
+ ],
}
config = FilterByPolygon(json, points, editing=False)
diff --git a/test/test_server_core.py b/test/test_server_core.py
index 77749dcb..16a58547 100755
--- a/test/test_server_core.py
+++ b/test/test_server_core.py
@@ -78,9 +78,9 @@ def test_get_lizmap_layer_login_filter(self):
{
'loginFilteredLayers': {
'lines-geojson': {
- 'layerId': 'lines_7ddd81b1_8307_4aa2_8b7a_a0b7983f33e3'
- }
- }
+ 'layerId': 'lines_7ddd81b1_8307_4aa2_8b7a_a0b7983f33e3',
+ },
+ },
},
'lines-geojson'))
self.assertIsNone(get_lizmap_layer_login_filter(
@@ -88,9 +88,9 @@ def test_get_lizmap_layer_login_filter(self):
'loginFilteredLayers': {
'lines-geojson': {
'layerId': 'lines_7ddd81b1_8307_4aa2_8b7a_a0b7983f33e3',
- 'filterAttribute': 'name'
- }
- }
+ 'filterAttribute': 'name',
+ },
+ },
},
'lines-geojson'))
self.assertIsNone(get_lizmap_layer_login_filter(
@@ -98,9 +98,9 @@ def test_get_lizmap_layer_login_filter(self):
'loginFilteredLayers': {
'lines-geojson': {
'layerId': 'lines_7ddd81b1_8307_4aa2_8b7a_a0b7983f33e3',
- 'filterPrivate': 'False'
- }
- }
+ 'filterPrivate': 'False',
+ },
+ },
},
'lines-geojson'))
self.assertIsNone(get_lizmap_layer_login_filter(
@@ -108,9 +108,9 @@ def test_get_lizmap_layer_login_filter(self):
'loginFilteredLayers': {
'lines-geojson': {
'filterAttribute': 'name',
- 'filterPrivate': 'False'
- }
- }
+ 'filterPrivate': 'False',
+ },
+ },
},
'lines-geojson'))
@@ -120,9 +120,9 @@ def test_get_lizmap_layer_login_filter(self):
'layerId': 'lines_7ddd81b1_8307_4aa2_8b7a_a0b7983f33e3',
'filterAttribute': 'name',
'filterPrivate': 'False',
- 'order': 0
- }
- }
+ 'order': 0,
+ },
+ },
}
self.assertIsNone(get_lizmap_layer_login_filter(good_dict, 'foobar'))
@@ -137,7 +137,7 @@ def test_get_lizmap_layer_login_filter(self):
'layerId': 'lines_7ddd81b1_8307_4aa2_8b7a_a0b7983f33e3',
'filterAttribute': 'name',
'filterPrivate': 'False',
- 'order': 0
+ 'order': 0,
})
def test_parse_xml_get_feature_info(self):
@@ -196,7 +196,7 @@ def test_edit_xml_get_feature_info_without_maptip(self):
'''
self.assertEqual(
ET.tostring(ET.fromstring(expected)).decode("utf-8"),
- ET.tostring(ET.fromstring(response)).decode("utf-8")
+ ET.tostring(ET.fromstring(response)).decode("utf-8"),
)
def test_edit_xml_get_feature_info_with_maptip(self):
@@ -236,7 +236,7 @@ def test_edit_xml_get_feature_info_with_maptip(self):
'''
self.assertEqual(
ET.tostring(ET.fromstring(expected)).decode("utf-8"),
- ET.tostring(ET.fromstring(response)).decode("utf-8")
+ ET.tostring(ET.fromstring(response)).decode("utf-8"),
)
def test_feature_id_expression(self):
@@ -247,13 +247,13 @@ def test_feature_id_expression(self):
self.assertEqual(
"",
- _server_feature_id_expression("1", [], fields)
+ _server_feature_id_expression("1", [], fields),
)
self.assertEqual(
"\"field_1\" = '1'",
- _server_feature_id_expression("1", ['field_1'], fields)
+ _server_feature_id_expression("1", ['field_1'], fields),
)
self.assertEqual(
"\"field_1\" = '1' AND \"field_2\" = '2'",
- _server_feature_id_expression("1@@2", ['field_1', 'field_2'], fields)
+ _server_feature_id_expression("1@@2", ['field_1', 'field_2'], fields),
)
diff --git a/test/utils.py b/test/utils.py
index ff84deef..25be92d3 100644
--- a/test/utils.py
+++ b/test/utils.py
@@ -17,7 +17,7 @@ def _build_query_string(params: dict) -> str:
return query_string
-def _check_request(result, content_type: str = 'application/json', http_code=200):
+def _check_request(result, content_type: str = 'application/json', http_code: int = 200): # noqa ANN401
""" Check the output and return the content. """
assert result.status_code == http_code, f'HTTP code {result.status_code}, expected {http_code}'
assert result.headers.get('Content-Type', '').lower().find(content_type) == 0, f'Headers {result.headers}'