Skip to content
Browse files
[api][needs-docs] Allow registering PyQGIS using a nice decorator syntax
This allows nice and simple, elegant construction of checks for

To use, Python based checks should use the decorator syntax:

  from qgis.core import check

  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:

  def layout_map_crs_choice_check(context, feedback):
    layout = context.layout
    results = []
    for i in layout.items():
      if isinstance(i, QgsLayoutItemMap) and == '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())

    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/
  2. +95 −0 python/core/additions/
  3. +27 −1 tests/src/python/
@@ -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 -*-

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 (

class CheckFactory:
Constructs QgsAbstractValidityChecks using a decorator.
To use, Python based checks should use the decorator syntax:
.. highlight:: python
.. code-block:: python
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)

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
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 @@
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
def my_check(context, feedback):
assert context

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

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()

0 comments on commit fdfe0ce

Please sign in to comment.