diff --git a/python/core/__init__.py b/python/core/__init__.py index 50d37024b06..95345ba3c7f 100644 --- a/python/core/__init__.py +++ b/python/core/__init__.py @@ -16,8 +16,6 @@ * * *************************************************************************** """ -from builtins import str -from builtins import object __author__ = 'Nathan Woodrow' __date__ = 'May 2014' @@ -25,341 +23,25 @@ __copyright__ = '(C) 2014, Nathan Woodrow' # This will get replaced with a git SHA1 when you do a git archive __revision__ = '$Format:%H$' -from qgis.PyQt.QtCore import QCoreApplication, NULL -import inspect -import string -import types -import functools from qgis._core import * +from .additions.readwritecontextentercategory import ReadWriteContextEnterCategory +from .additions.projectdirtyblocker import ProjectDirtyBlocker +from .additions.qgstaskwrapper import QgsTaskWrapper +from .additions.qgsfunction import register_function, qgsfunction +from .additions.edit import edit, QgsEditError +from .additions.fromfunction import fromFunction +from .additions.processing import processing_output_layer_repr, processing_source_repr +from .additions.qgsgeometry import _geometryNonZero +from .additions.qgsdefaultvalue import _isValid -# Boolean evaluation of QgsGeometry - - -def _geometryNonZero(self): - return not self.isEmpty() - - -def _isValid(self): - return self.isValid() - - +# Injections into classes QgsGeometry.__nonzero__ = _geometryNonZero QgsGeometry.__bool__ = _geometryNonZero - QgsDefaultValue.__bool__ = _isValid - - -def register_function(function, arg_count, group, usesgeometry=False, - referenced_columns=[QgsFeatureRequest.ALL_ATTRIBUTES], **kwargs): - """ - Register a Python function to be used as a expression function. - - Functions should take (values, feature, parent) as args: - - Example: - def myfunc(values, feature, parent): - pass - - They can also shortcut naming feature and parent args by using *args - if they are not needed in the function. - - Example: - def myfunc(values, *args): - pass - - Functions should return a value compatible with QVariant - - Eval errors can be raised using parent.setEvalErrorString("Error message") - - :param function: - :param arg_count: - :param group: - :param usesgeometry: - :return: - """ - - class QgsPyExpressionFunction(QgsExpressionFunction): - - def __init__(self, func, name, args, group, helptext='', usesGeometry=True, - referencedColumns=QgsFeatureRequest.ALL_ATTRIBUTES, expandargs=False): - QgsExpressionFunction.__init__(self, name, args, group, helptext) - self.function = func - self.expandargs = expandargs - self.uses_geometry = usesGeometry - self.referenced_columns = referencedColumns - - def func(self, values, context, parent, node): - feature = None - if context: - feature = context.feature() - - try: - if self.expandargs: - values.append(feature) - values.append(parent) - if inspect.getargspec(self.function).args[-1] == 'context': - values.append(context) - return self.function(*values) - else: - if inspect.getargspec(self.function).args[-1] == 'context': - self.function(values, feature, parent, context) - return self.function(values, feature, parent) - except Exception as ex: - parent.setEvalErrorString(str(ex)) - return None - - def usesGeometry(self, node): - return self.uses_geometry - - def referencedColumns(self, node): - return self.referenced_columns - - helptemplate = string.Template("""

$name function


$doc""") - name = kwargs.get('name', function.__name__) - helptext = kwargs.get('helpText') or function.__doc__ or '' - helptext = helptext.strip() - expandargs = False - - if arg_count == "auto": - # Work out the number of args we need. - # Number of function args - 2. The last two args are always feature, parent. - args = inspect.getargspec(function).args - number = len(args) - arg_count = number - 2 - if args[-1] == 'context': - arg_count -= 1 - expandargs = True - - register = kwargs.get('register', True) - if register and QgsExpression.isFunctionName(name): - if not QgsExpression.unregisterFunction(name): - msgtitle = QCoreApplication.translate("UserExpressions", "User expressions") - msg = QCoreApplication.translate("UserExpressions", - "The user expression {0} already exists and could not be unregistered.").format( - name) - QgsMessageLog.logMessage(msg + "\n", msgtitle, Qgis.Warning) - return None - - function.__name__ = name - helptext = helptemplate.safe_substitute(name=name, doc=helptext) - f = QgsPyExpressionFunction(function, name, arg_count, group, helptext, usesgeometry, referenced_columns, - expandargs) - - # This doesn't really make any sense here but does when used from a decorator context - # so it can stay. - if register: - QgsExpression.registerFunction(f) - return f - - -def qgsfunction(args='auto', group='custom', **kwargs): - """ - Decorator function used to define a user expression function. - - Example: - @qgsfunction(2, 'test'): - def add(values, feature, parent): - pass - - Will create and register a function in QgsExpression called 'add' in the - 'test' group that takes two arguments. - - or not using feature and parent: - - Example: - @qgsfunction(2, 'test'): - def add(values, *args): - pass - """ - - def wrapper(func): - return register_function(func, args, group, **kwargs) - - return wrapper - - -class QgsEditError(Exception): - - def __init__(self, value): - self.value = value - - def __str__(self): - return repr(self.value) - - -# Define a `with edit(layer)` statement - - -class edit(object): - - def __init__(self, layer): - self.layer = layer - - def __enter__(self): - assert self.layer.startEditing() - return self.layer - - def __exit__(self, ex_type, ex_value, traceback): - if ex_type is None: - if not self.layer.commitChanges(): - raise QgsEditError(self.layer.commitErrors()) - return True - else: - self.layer.rollBack() - return False - -# Python class to mimic QgsReadWriteContextCategoryPopper C++ class - - -class ReadWriteContextEnterCategory(): - """ - Push a category to the stack - - .. code-block:: python - - context = QgsReadWriteContext() - with QgsReadWriteContext.enterCategory(context, category, details): - # do something - - .. versionadded:: 3.2 - """ - - def __init__(self, context, category_name, details=None): - self.context = context - self.category_name = category_name - self.details = details - self.popper = None - - def __enter__(self): - self.popper = self.context._enterCategory(self.category_name, self.details) - return self.context - - def __exit__(self, ex_type, ex_value, traceback): - del self.popper - return True - - -# Inject the context manager into QgsReadWriteContext class as a member QgsReadWriteContext.enterCategory = ReadWriteContextEnterCategory - - -# Python class to extend QgsProjectDirtyBlocker C++ class - - -class ProjectDirtyBlocker(): - """ - Context manager used to block project setDirty calls. - - .. code-block:: python - - project = QgsProject.instance() - with QgsProject.blockDirtying(project): - # do something - - .. versionadded:: 3.2 - """ - - def __init__(self, project): - self.project = project - self.blocker = None - - def __enter__(self): - self.blocker = QgsProjectDirtyBlocker(self.project) - return self.project - - def __exit__(self, ex_type, ex_value, traceback): - del self.blocker - return True - - -# Inject the context manager into QgsProject class as a member QgsProject.blockDirtying = ProjectDirtyBlocker - - -class QgsTaskWrapper(QgsTask): - - def __init__(self, description, flags, function, on_finished, *args, **kwargs): - QgsTask.__init__(self, description, flags) - self.args = args - self.kwargs = kwargs - self.function = function - self.on_finished = on_finished - self.returned_values = None - self.exception = None - - def run(self): - try: - self.returned_values = self.function(self, *self.args, **self.kwargs) - except Exception as ex: - # report error - self.exception = ex - return False - - return True - - def finished(self, result): - if not self.on_finished: - return - - if not result and self.exception is None: - self.exception = Exception('Task canceled') - - try: - if self.returned_values: - self.on_finished(self.exception, self.returned_values) - else: - self.on_finished(self.exception) - except Exception as ex: - self.exception = ex - - -@staticmethod -def fromFunction(description, function, *args, on_finished=None, flags=QgsTask.AllFlags, **kwargs): - """ - Creates a new QgsTask task from a python function. - - Example: - - def calculate(task): - # pretend this is some complex maths and stuff we want - # to run in the background - return 5*6 - - def calculation_finished(exception, value=None): - if not exception: - iface.messageBar().pushMessage( - 'the magic number is {}'.format(value)) - else: - iface.messageBar().pushMessage( - str(exception)) - - task = QgsTask.fromFunction('my task', calculate, - on_finished=calculation_finished) - QgsApplication.taskManager().addTask(task) - - """ - - assert function - return QgsTaskWrapper(description, flags, function, on_finished, *args, **kwargs) - - QgsTask.fromFunction = fromFunction - - -# add some __repr__ methods to processing classes -def processing_source_repr(self): - return "".format( - self.source.staticValue(), self.selectedFeaturesOnly) - - QgsProcessingFeatureSourceDefinition.__repr__ = processing_source_repr - - -def processing_output_layer_repr(self): - return "".format(self.sink.staticValue(), - self.createOptions) - - QgsProcessingOutputLayerDefinition.__repr__ = processing_output_layer_repr diff --git a/python/core/additions/__init__.py b/python/core/additions/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/python/core/additions/edit.py b/python/core/additions/edit.py new file mode 100644 index 00000000000..92756d1e895 --- /dev/null +++ b/python/core/additions/edit.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- + +""" +*************************************************************************** + edit.py + --------------------- + Date : May 2018 + Copyright : (C) 2018 by Denis Rouzaud + Email : denis@opengis.ch +*************************************************************************** +* * +* 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 builtins import object + + +class QgsEditError(Exception): + + def __init__(self, value): + self.value = value + + def __str__(self): + return repr(self.value) + + +class edit(object): + + def __init__(self, layer): + self.layer = layer + + def __enter__(self): + assert self.layer.startEditing() + return self.layer + + def __exit__(self, ex_type, ex_value, traceback): + if ex_type is None: + if not self.layer.commitChanges(): + raise QgsEditError(self.layer.commitErrors()) + return True + else: + self.layer.rollBack() + return False diff --git a/python/core/additions/fromfunction.py b/python/core/additions/fromfunction.py new file mode 100644 index 00000000000..79f19d3810f --- /dev/null +++ b/python/core/additions/fromfunction.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- + +""" +*************************************************************************** + fromfunction.py + --------------------- + Date : May 2018 + Copyright : (C) 2018 by Denis Rouzaud + Email : denis@opengis.ch +*************************************************************************** +* * +* 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 .qgstaskwrapper import QgsTaskWrapper +from qgis._core import QgsTask + + +@staticmethod +def fromFunction(description, function, *args, on_finished=None, flags=QgsTask.AllFlags, **kwargs): + """ + Creates a new QgsTask task from a python function. + + Example: + + def calculate(task): + # pretend this is some complex maths and stuff we want + # to run in the background + return 5*6 + + def calculation_finished(exception, value=None): + if not exception: + iface.messageBar().pushMessage( + 'the magic number is {}'.format(value)) + else: + iface.messageBar().pushMessage( + str(exception)) + + task = QgsTask.fromFunction('my task', calculate, + on_finished=calculation_finished) + QgsApplication.taskManager().addTask(task) + + """ + + assert function + return QgsTaskWrapper(description, flags, function, on_finished, *args, **kwargs) diff --git a/python/core/additions/processing.py b/python/core/additions/processing.py new file mode 100644 index 00000000000..95545e674ba --- /dev/null +++ b/python/core/additions/processing.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- + +""" +*************************************************************************** + processing.py + --------------------- + Date : May 2018 + Copyright : (C) 2018 by Denis Rouzaud + Email : denis@opengis.ch +*************************************************************************** +* * +* 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. * +* * +*************************************************************************** +""" + + +# add some __repr__ methods to processing classes +def processing_source_repr(self): + return "".format( + self.source.staticValue(), self.selectedFeaturesOnly) + + +def processing_output_layer_repr(self): + return "".format(self.sink.staticValue(), + self.createOptions) diff --git a/python/core/additions/projectdirtyblocker.py b/python/core/additions/projectdirtyblocker.py new file mode 100644 index 00000000000..bdaf7c99386 --- /dev/null +++ b/python/core/additions/projectdirtyblocker.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- + +""" +*************************************************************************** + projectdirtyblocker.py + --------------------- + Date : May 2018 + Copyright : (C) 2018 by Denis Rouzaud + Email : denis@opengis.ch +*************************************************************************** +* * +* 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 QgsProjectDirtyBlocker + + +class ProjectDirtyBlocker(): + """ + Context manager used to block project setDirty calls. + + .. code-block:: python + + project = QgsProject.instance() + with QgsProject.blockDirtying(project): + # do something + + .. versionadded:: 3.2 + """ + + def __init__(self, project): + self.project = project + self.blocker = None + + def __enter__(self): + self.blocker = QgsProjectDirtyBlocker(self.project) + return self.project + + def __exit__(self, ex_type, ex_value, traceback): + del self.blocker + return True diff --git a/python/core/additions/qgsdefaultvalue.py b/python/core/additions/qgsdefaultvalue.py new file mode 100644 index 00000000000..6daf2a40540 --- /dev/null +++ b/python/core/additions/qgsdefaultvalue.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- + +""" +*************************************************************************** + qgsdefaultvalue.py + --------------------- + Date : May 2018 + Copyright : (C) 2018 by Denis Rouzaud + Email : denis@opengis.ch +*************************************************************************** +* * +* 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. * +* * +*************************************************************************** +""" + + +def _isValid(self): + return self.isValid() diff --git a/python/core/additions/qgsfunction.py b/python/core/additions/qgsfunction.py new file mode 100644 index 00000000000..1e231340c71 --- /dev/null +++ b/python/core/additions/qgsfunction.py @@ -0,0 +1,154 @@ +# -*- coding: utf-8 -*- + +""" +*************************************************************************** + qgsfunction.py + --------------------- + Date : May 2018 + Copyright : (C) 2018 by Denis Rouzaud + Email : denis@opengis.ch +*************************************************************************** +* * +* 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. * +* * +*************************************************************************** +""" + + +import inspect +import string +from builtins import str +from qgis.PyQt.QtCore import QCoreApplication +from qgis._core import QgsExpressionFunction, QgsExpression, QgsMessageLog, QgsFeatureRequest + + +def register_function(function, arg_count, group, usesgeometry=False, + referenced_columns=[QgsFeatureRequest.ALL_ATTRIBUTES], **kwargs): + """ + Register a Python function to be used as a expression function. + + Functions should take (values, feature, parent) as args: + + Example: + def myfunc(values, feature, parent): + pass + + They can also shortcut naming feature and parent args by using *args + if they are not needed in the function. + + Example: + def myfunc(values, *args): + pass + + Functions should return a value compatible with QVariant + + Eval errors can be raised using parent.setEvalErrorString("Error message") + + :param function: + :param arg_count: + :param group: + :param usesgeometry: + :return: + """ + + class QgsPyExpressionFunction(QgsExpressionFunction): + + def __init__(self, func, name, args, group, helptext='', usesGeometry=True, + referencedColumns=QgsFeatureRequest.ALL_ATTRIBUTES, expandargs=False): + QgsExpressionFunction.__init__(self, name, args, group, helptext) + self.function = func + self.expandargs = expandargs + self.uses_geometry = usesGeometry + self.referenced_columns = referencedColumns + + def func(self, values, context, parent, node): + feature = None + if context: + feature = context.feature() + + try: + if self.expandargs: + values.append(feature) + values.append(parent) + if inspect.getargspec(self.function).args[-1] == 'context': + values.append(context) + return self.function(*values) + else: + if inspect.getargspec(self.function).args[-1] == 'context': + self.function(values, feature, parent, context) + return self.function(values, feature, parent) + except Exception as ex: + parent.setEvalErrorString(str(ex)) + return None + + def usesGeometry(self, node): + return self.uses_geometry + + def referencedColumns(self, node): + return self.referenced_columns + + helptemplate = string.Template("""

$name function


$doc""") + name = kwargs.get('name', function.__name__) + helptext = kwargs.get('helpText') or function.__doc__ or '' + helptext = helptext.strip() + expandargs = False + + if arg_count == "auto": + # Work out the number of args we need. + # Number of function args - 2. The last two args are always feature, parent. + args = inspect.getargspec(function).args + number = len(args) + arg_count = number - 2 + if args[-1] == 'context': + arg_count -= 1 + expandargs = True + + register = kwargs.get('register', True) + if register and QgsExpression.isFunctionName(name): + if not QgsExpression.unregisterFunction(name): + msgtitle = QCoreApplication.translate("UserExpressions", "User expressions") + msg = QCoreApplication.translate("UserExpressions", + "The user expression {0} already exists and could not be unregistered.").format( + name) + QgsMessageLog.logMessage(msg + "\n", msgtitle, Qgis.Warning) + return None + + function.__name__ = name + helptext = helptemplate.safe_substitute(name=name, doc=helptext) + f = QgsPyExpressionFunction(function, name, arg_count, group, helptext, usesgeometry, referenced_columns, + expandargs) + + # This doesn't really make any sense here but does when used from a decorator context + # so it can stay. + if register: + QgsExpression.registerFunction(f) + return f + + +def qgsfunction(args='auto', group='custom', **kwargs): + """ + Decorator function used to define a user expression function. + + Example: + @qgsfunction(2, 'test'): + def add(values, feature, parent): + pass + + Will create and register a function in QgsExpression called 'add' in the + 'test' group that takes two arguments. + + or not using feature and parent: + + Example: + @qgsfunction(2, 'test'): + def add(values, *args): + pass + """ + + def wrapper(func): + return register_function(func, args, group, **kwargs) + + return wrapper diff --git a/python/core/additions/qgsgeometry.py b/python/core/additions/qgsgeometry.py new file mode 100644 index 00000000000..cc25a0fffd5 --- /dev/null +++ b/python/core/additions/qgsgeometry.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- + +""" +*************************************************************************** + qgsgeometry.py + --------------------- + Date : May 2018 + Copyright : (C) 2018 by Denis Rouzaud + Email : denis@opengis.ch +*************************************************************************** +* * +* 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. * +* * +*************************************************************************** +""" + + +def _geometryNonZero(self): + return not self.isEmpty() diff --git a/python/core/additions/qgstaskwrapper.py b/python/core/additions/qgstaskwrapper.py new file mode 100644 index 00000000000..6c2b7560d81 --- /dev/null +++ b/python/core/additions/qgstaskwrapper.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- + +""" +*************************************************************************** + qgstaskwrapper.py + --------------------- + Date : May 2018 + Copyright : (C) 2018 by Denis Rouzaud + Email : denis@opengis.ch +*************************************************************************** +* * +* 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 QgsTask + + +class QgsTaskWrapper(QgsTask): + + def __init__(self, description, flags, function, on_finished, *args, **kwargs): + QgsTask.__init__(self, description, flags) + self.args = args + self.kwargs = kwargs + self.function = function + self.on_finished = on_finished + self.returned_values = None + self.exception = None + + def run(self): + try: + self.returned_values = self.function(self, *self.args, **self.kwargs) + except Exception as ex: + # report error + self.exception = ex + return False + + return True + + def finished(self, result): + if not self.on_finished: + return + + if not result and self.exception is None: + self.exception = Exception('Task canceled') + + try: + if self.returned_values: + self.on_finished(self.exception, self.returned_values) + else: + self.on_finished(self.exception) + except Exception as ex: + self.exception = ex diff --git a/python/core/additions/readwritecontextentercategory.py b/python/core/additions/readwritecontextentercategory.py new file mode 100644 index 00000000000..999e0a94e88 --- /dev/null +++ b/python/core/additions/readwritecontextentercategory.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- + +""" +*************************************************************************** + readwritecontextentercategory.py + --------------------- + Date : May 2018 + Copyright : (C) 2018 by Denis Rouzaud + Email : denis@opengis.ch +*************************************************************************** +* * +* 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. * +* * +*************************************************************************** +""" + + +class ReadWriteContextEnterCategory(): + """ + Push a category to the stack + + .. code-block:: python + + context = QgsReadWriteContext() + with QgsReadWriteContext.enterCategory(context, category, details): + # do something + + .. versionadded:: 3.2 + """ + + def __init__(self, context, category_name, details=None): + self.context = context + self.category_name = category_name + self.details = details + self.popper = None + + def __enter__(self): + self.popper = self.context._enterCategory(self.category_name, self.details) + return self.context + + def __exit__(self, ex_type, ex_value, traceback): + del self.popper + return True