From fdfe0cee2399efa66a7503624b016eb6b4912707 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 10 Jan 2019 14:52:01 +1000 Subject: [PATCH] [api][needs-docs] Allow registering PyQGIS using a nice decorator syntax This allows nice and simple, elegant construction of checks for Python. To use, Python based checks should use the decorator syntax: from qgis.core import check @check.register(type=QgsAbstractValidityCheck.TypeLayoutCheck) def my_layout_check(context, feedback): results = ... return results Or, a more complete example. This one throws a warning when attempting to export a layout with a map item set to the Web Mercator projection: @check.register(type=QgsAbstractValidityCheck.TypeLayoutCheck) def layout_map_crs_choice_check(context, feedback): layout = context.layout results = [] for i in layout.items(): if isinstance(i, QgsLayoutItemMap) and i.crs().authid() == 'EPSG:3857': res = QgsValidityCheckResult() res.type = QgsValidityCheckResult.Warning res.title='Map projection is misleading' res.detailedDescription='The projection for the map item {} is set to Web Mercator (EPSG:3857) which misrepresents areas and shapes. Consider using an appropriate local projection instead.'.format(i.displayName()) results.append(res) return results --- python/core/__init__.py.in | 1 + python/core/additions/validitycheck.py | 95 ++++++++++++++++++++++ tests/src/python/test_qgsvaliditychecks.py | 28 ++++++- 3 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 python/core/additions/validitycheck.py diff --git a/python/core/__init__.py.in b/python/core/__init__.py.in index 94925cf61e5..0a5de109653 100644 --- a/python/core/__init__.py.in +++ b/python/core/__init__.py.in @@ -37,6 +37,7 @@ from .additions.qgsgeometry import _geometryNonZero, mapping_geometry from .additions.qgssettings import _qgssettings_enum_value, _qgssettings_set_enum_value, _qgssettings_flag_value from .additions.qgstaskwrapper import QgsTaskWrapper from .additions.readwritecontextentercategory import ReadWriteContextEnterCategory +from .additions.validitycheck import check # Injections into classes QgsFeature.__geo_interface__ = property(mapping_feature) diff --git a/python/core/additions/validitycheck.py b/python/core/additions/validitycheck.py new file mode 100644 index 00000000000..25fdd1586f0 --- /dev/null +++ b/python/core/additions/validitycheck.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- + +""" +*************************************************************************** + validitycheck.py + --------------------- + Date : January 2019 + Copyright : (C) 2019 by Nyall Dawson + Email : nyall dot dawson at gmail dot com +*************************************************************************** +* * +* This program is free software; you can redistribute it and/or modify * +* it under the terms of the GNU General Public License as published by * +* the Free Software Foundation; either version 2 of the License, or * +* (at your option) any later version. * +* * +*************************************************************************** +""" +from qgis._core import ( + QgsAbstractValidityCheck, + QgsApplication) + + +class CheckFactory: + """ + Constructs QgsAbstractValidityChecks using a decorator. + + To use, Python based checks should use the decorator syntax: + + .. highlight:: python + .. code-block:: python + @check.register(type=QgsAbstractValidityCheck.TypeLayoutCheck) + def my_layout_check(context, feedback): + results = ... + return results + + """ + + def __init__(self): + # unfortunately /Transfer/ annotation isn't working correct on validityCheckRegistry().addCheck(), + # so we manually need to store a reference to all checks we register + self.checks = [] + + def register(self, type, *args, **kwargs): + """ + Implements a decorator for registering Python based checks. + + :param type: check type, e.g. QgsAbstractValidityCheck.TypeLayoutCheck + """ + + def dec(f): + check = CheckWrapper(check_type=type, check_func=f) + self.checks.append(check) + QgsApplication.validityCheckRegistry().addCheck(check) + + return dec + + +class CheckWrapper(QgsAbstractValidityCheck): + """ + Wrapper object used to create new validity checks from @check. + """ + + def __init__(self, check_type, check_func): + """ + Initializer for CheckWrapper. + + :param check_type: check type, e.g. QgsAbstractValidityCheck.TypeLayoutCheck + :param check_func: test function, should return a list of QgsValidityCheckResult results + """ + super().__init__() + self._check_type = check_type + self._results = [] + self._check_func = check_func + + def create(self): + return CheckWrapper(check_type=self._check_type, check_func=self._check_func) + + def id(self): + return self._check_func.__name__ + + def checkType(self): + return self._check_type + + def prepareCheck(self, context, feedback): + self._results = self._check_func(context, feedback) + if self._results is None: + self._results = [] + return True + + def runCheck(self, context, feedback): + return self._results + + +check = CheckFactory() diff --git a/tests/src/python/test_qgsvaliditychecks.py b/tests/src/python/test_qgsvaliditychecks.py index 3ff7c2f85aa..c974a0f3c64 100644 --- a/tests/src/python/test_qgsvaliditychecks.py +++ b/tests/src/python/test_qgsvaliditychecks.py @@ -19,7 +19,8 @@ from qgis.core import (QgsApplication, QgsValidityCheckRegistry, QgsValidityCheckResult, QgsValidityCheckContext, - QgsFeedback) + QgsFeedback, + check) from qgis.testing import start_app, unittest app = start_app() @@ -53,12 +54,37 @@ class TestContext(QgsValidityCheckContext): return 0 +# register some checks using the decorator syntax +@check.register(type=QgsAbstractValidityCheck.TypeLayoutCheck) +def my_check(context, feedback): + assert context + + +@check.register(type=QgsAbstractValidityCheck.TypeLayoutCheck) +def my_check2(context, feedback): + res = QgsValidityCheckResult() + res.type = QgsValidityCheckResult.Warning + res.title = 'test' + res.detailedDescription = 'blah blah' + return [res] + + class TestQgsValidityChecks(unittest.TestCase): def testAppRegistry(self): # ensure there is an application instance self.assertIsNotNone(QgsApplication.validityCheckRegistry()) + def testDecorator(self): + # test that checks registered using the decorator have worked + self.assertEqual(len(QgsApplication.validityCheckRegistry().checks()), 2) + + context = TestContext() + feedback = QgsFeedback() + res = QgsApplication.validityCheckRegistry().runChecks(QgsAbstractValidityCheck.TypeLayoutCheck, context, feedback) + self.assertEqual(len(res), 1) + self.assertEqual(res[0].title, 'test') + def testRegistry(self): registry = QgsValidityCheckRegistry() self.assertFalse(registry.checks())