Skip to content

Commit

Permalink
[api][needs-docs] Allow registering PyQGIS using a nice decorator syntax
Browse files Browse the repository at this point in the history
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 fdfe0ce
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 1 deletion.
1 change: 1 addition & 0 deletions python/core/__init__.py.in
Expand Up @@ -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)
Expand Down
95 changes: 95 additions & 0 deletions 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()
28 changes: 27 additions & 1 deletion tests/src/python/test_qgsvaliditychecks.py
Expand Up @@ -19,7 +19,8 @@
QgsValidityCheckRegistry,
QgsValidityCheckResult,
QgsValidityCheckContext,
QgsFeedback)
QgsFeedback,
check)
from qgis.testing import start_app, unittest

app = start_app()
Expand Down Expand Up @@ -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())
Expand Down

0 comments on commit fdfe0ce

Please sign in to comment.