Skip to content
Permalink
Browse files

[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 <i>Web Mercator (EPSG:3857)</i> which misrepresents areas and shapes. Consider using an appropriate local projection instead.'.format(i.displayName())
        results.append(res)

    return results
  • Loading branch information
nyalldawson committed Jan 10, 2019
1 parent fd001bb commit fdfe0cee2399efa66a7503624b016eb6b4912707
Showing with 123 additions and 1 deletion.
  1. +1 −0 python/core/__init__.py.in
  2. +95 −0 python/core/additions/validitycheck.py
  3. +27 −1 tests/src/python/test_qgsvaliditychecks.py
@@ -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)
@@ -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()
@@ -19,7 +19,8 @@
QgsValidityCheckRegistry,
QgsValidityCheckResult,
QgsValidityCheckContext,
QgsFeedback)
QgsFeedback,
check)
from qgis.testing import start_app, unittest

app = start_app()
@@ -53,12 +54,37 @@ def type(self):
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())

0 comments on commit fdfe0ce

Please sign in to comment.
You can’t perform that action at this time.