def test_create_nulls_and_defaults(self): """Test bug #21304 when pasting features from another layer and default values are not honored""" vl = createLayerWithOnePoint() vl.setDefaultValueDefinition(1, QgsDefaultValue('300')) features_data = [] context = vl.createExpressionContext() features_data.append(QgsVectorLayerUtils.QgsFeatureData(QgsGeometry.fromWkt('Point (7 44)'), {0: 'test_1', 1: None})) features_data.append(QgsVectorLayerUtils.QgsFeatureData(QgsGeometry.fromWkt('Point (7 45)'), {0: 'test_2', 1: QVariant()})) features_data.append(QgsVectorLayerUtils.QgsFeatureData(QgsGeometry.fromWkt('Point (7 46)'), {0: 'test_3', 1: QVariant(QVariant.Int)})) features_data.append(QgsVectorLayerUtils.QgsFeatureData(QgsGeometry.fromWkt('Point (7 46)'), {0: 'test_4'})) features = QgsVectorLayerUtils.createFeatures(vl, features_data, context) for f in features: self.assertEqual(f.attributes()[1], 300, f.id()) vl = createLayerWithOnePoint() vl.setDefaultValueDefinition(0, QgsDefaultValue("'my_default'")) features_data = [] context = vl.createExpressionContext() features_data.append(QgsVectorLayerUtils.QgsFeatureData(QgsGeometry.fromWkt('Point (7 44)'), {0: None})) features_data.append(QgsVectorLayerUtils.QgsFeatureData(QgsGeometry.fromWkt('Point (7 45)'), {0: QVariant()})) features_data.append(QgsVectorLayerUtils.QgsFeatureData(QgsGeometry.fromWkt('Point (7 46)'), {0: QVariant(QVariant.String)})) features_data.append(QgsVectorLayerUtils.QgsFeatureData(QgsGeometry.fromWkt('Point (7 46)'), {})) features = QgsVectorLayerUtils.createFeatures(vl, features_data, context) for f in features: self.assertEqual(f.attributes()[0], 'my_default', f.id())
def test_unique_pk_when_subset(self): """Test unique values on filtered layer GH #30062""" src = unitTestDataPath('points_gpkg.gpkg') dest = tempfile.mktemp() + '.gpkg' shutil.copy(src, dest) vl = QgsVectorLayer(dest, 'vl', 'ogr') self.assertTrue(vl.isValid()) features_data = [] it = vl.getFeatures() for _ in range(3): f = next(it) features_data.append( QgsVectorLayerUtils.QgsFeatureData( f.geometry(), dict(zip(range(f.fields().count()), f.attributes())))) # Set a filter vl.setSubsetString('"fid" in (4,5,6)') self.assertTrue(vl.isValid()) context = vl.createExpressionContext() features = QgsVectorLayerUtils.createFeatures(vl, features_data, context) self.assertTrue(vl.startEditing()) vl.addFeatures(features) self.assertTrue(vl.commitChanges())
def test_create_multiple_unique_constraint(self): """Test create multiple features with unique constraint""" vl = createLayerWithOnePoint() vl.setFieldConstraint(1, QgsFieldConstraints.ConstraintUnique) features_data = [] context = vl.createExpressionContext() for i in range(2): features_data.append(QgsVectorLayerUtils.QgsFeatureData(QgsGeometry.fromWkt('Point (7 44)'), {0: 'test_%s' % i, 1: 123})) features = QgsVectorLayerUtils.createFeatures(vl, features_data, context) self.assertEqual(features[0].attributes()[1], 124) self.assertEqual(features[1].attributes()[1], 125)
def execute_in_place_run(alg, parameters, context=None, feedback=None, raise_exceptions=False): """Executes an algorithm modifying features in-place in the input layer. :param alg: algorithm to run :type alg: QgsProcessingAlgorithm :param parameters: parameters of the algorithm :type parameters: dict :param context: context, defaults to None :type context: QgsProcessingContext, optional :param feedback: feedback, defaults to None :type feedback: QgsProcessingFeedback, optional :param raise_exceptions: useful for testing, if True exceptions are raised, normally exceptions will be forwarded to the feedback :type raise_exceptions: boo, default to False :raises QgsProcessingException: raised when there is no active layer, or it cannot be made editable :return: a tuple with true if success and results :rtype: tuple """ if feedback is None: feedback = QgsProcessingFeedback() if context is None: context = dataobjects.createContext(feedback) # Only feature based algs have sourceFlags try: if alg.sourceFlags() & QgsProcessingFeatureSource.FlagSkipGeometryValidityChecks: context.setInvalidGeometryCheck(QgsFeatureRequest.GeometryNoCheck) except AttributeError: pass active_layer = parameters['INPUT'] # Run some checks and prepare the layer for in-place execution by: # - getting the active layer and checking that it is a vector # - making the layer editable if it was not already # - selecting all features if none was selected # - checking in-place support for the active layer/alg/parameters # If one of the check fails and raise_exceptions is True an exception # is raised, else the execution is aborted and the error reported in # the feedback try: if active_layer is None: raise QgsProcessingException(tr("There is not active layer.")) if not isinstance(active_layer, QgsVectorLayer): raise QgsProcessingException(tr("Active layer is not a vector layer.")) if not active_layer.isEditable(): if not active_layer.startEditing(): raise QgsProcessingException(tr("Active layer is not editable (and editing could not be turned on).")) if not alg.supportInPlaceEdit(active_layer): raise QgsProcessingException(tr("Selected algorithm and parameter configuration are not compatible with in-place modifications.")) except QgsProcessingException as e: if raise_exceptions: raise e QgsMessageLog.logMessage(str(sys.exc_info()[0]), 'Processing', Qgis.Critical) if feedback is not None: feedback.reportError(getattr(e, 'msg', str(e)), fatalError=True) return False, {} if not active_layer.selectedFeatureIds(): active_layer.selectAll() # Make sure we are working on selected features only parameters['INPUT'] = QgsProcessingFeatureSourceDefinition(active_layer.id(), True) parameters['OUTPUT'] = 'memory:' req = QgsFeatureRequest(QgsExpression(r"$id < 0")) req.setFlags(QgsFeatureRequest.NoGeometry) req.setSubsetOfAttributes([]) # Start the execution # If anything goes wrong and raise_exceptions is True an exception # is raised, else the execution is aborted and the error reported in # the feedback try: new_feature_ids = [] active_layer.beginEditCommand(alg.displayName()) # Checks whether the algorithm has a processFeature method if hasattr(alg, 'processFeature'): # in-place feature editing # Make a clone or it will crash the second time the dialog # is opened and run alg = alg.create({'IN_PLACE': True}) if not alg.prepare(parameters, context, feedback): raise QgsProcessingException(tr("Could not prepare selected algorithm.")) # Check again for compatibility after prepare if not alg.supportInPlaceEdit(active_layer): raise QgsProcessingException(tr("Selected algorithm and parameter configuration are not compatible with in-place modifications.")) # some algorithms have logic in outputFields/outputCrs/outputWkbType which they require to execute before # they can start processing features _ = alg.outputFields(active_layer.fields()) _ = alg.outputWkbType(active_layer.wkbType()) _ = alg.outputCrs(active_layer.crs()) field_idxs = range(len(active_layer.fields())) iterator_req = QgsFeatureRequest(active_layer.selectedFeatureIds()) iterator_req.setInvalidGeometryCheck(context.invalidGeometryCheck()) feature_iterator = active_layer.getFeatures(iterator_req) step = 100 / len(active_layer.selectedFeatureIds()) if active_layer.selectedFeatureIds() else 1 for current, f in enumerate(feature_iterator): if feedback.isCanceled(): break # need a deep copy, because python processFeature implementations may return # a shallow copy from processFeature input_feature = QgsFeature(f) new_features = alg.processFeature(input_feature, context, feedback) new_features = QgsVectorLayerUtils.makeFeaturesCompatible(new_features, active_layer) if len(new_features) == 0: active_layer.deleteFeature(f.id()) elif len(new_features) == 1: new_f = new_features[0] if not f.geometry().equals(new_f.geometry()): active_layer.changeGeometry(f.id(), new_f.geometry()) if f.attributes() != new_f.attributes(): active_layer.changeAttributeValues(f.id(), dict(zip(field_idxs, new_f.attributes())), dict(zip(field_idxs, f.attributes()))) new_feature_ids.append(f.id()) else: active_layer.deleteFeature(f.id()) # Get the new ids old_ids = set([f.id() for f in active_layer.getFeatures(req)]) # If multiple new features were created, we need to pass # them to createFeatures to manage constraints correctly features_data = [] for f in new_features: features_data.append(QgsVectorLayerUtils.QgsFeatureData(f.geometry(), dict(enumerate(f.attributes())))) new_features = QgsVectorLayerUtils.createFeatures(active_layer, features_data, context.expressionContext()) if not active_layer.addFeatures(new_features): raise QgsProcessingException(tr("Error adding processed features back into the layer.")) new_ids = set([f.id() for f in active_layer.getFeatures(req)]) new_feature_ids += list(new_ids - old_ids) feedback.setProgress(int((current + 1) * step)) results, ok = {'__count': current + 1}, True else: # Traditional 'run' with delete and add features cycle # There is no way to know if some features have been skipped # due to invalid geometries if context.invalidGeometryCheck() == QgsFeatureRequest.GeometrySkipInvalid: selected_ids = active_layer.selectedFeatureIds() else: selected_ids = [] results, ok = alg.run(parameters, context, feedback, configuration={'IN_PLACE': True}) if ok: result_layer = QgsProcessingUtils.mapLayerFromString(results['OUTPUT'], context) # TODO: check if features have changed before delete/add cycle new_features = [] # Check if there are any skipped features if context.invalidGeometryCheck() == QgsFeatureRequest.GeometrySkipInvalid: missing_ids = list(set(selected_ids) - set(result_layer.allFeatureIds())) if missing_ids: for f in active_layer.getFeatures(QgsFeatureRequest(missing_ids)): if not f.geometry().isGeosValid(): new_features.append(f) active_layer.deleteFeatures(active_layer.selectedFeatureIds()) for f in result_layer.getFeatures(): new_features.extend(QgsVectorLayerUtils. makeFeaturesCompatible([f], active_layer)) # Get the new ids old_ids = set([f.id() for f in active_layer.getFeatures(req)]) if not active_layer.addFeatures(new_features): raise QgsProcessingException(tr("Error adding processed features back into the layer.")) new_ids = set([f.id() for f in active_layer.getFeatures(req)]) new_feature_ids += list(new_ids - old_ids) results['__count'] = len(new_feature_ids) active_layer.endEditCommand() if ok and new_feature_ids: active_layer.selectByIds(new_feature_ids) elif not ok: active_layer.rollBack() return ok, results except QgsProcessingException as e: active_layer.endEditCommand() active_layer.rollBack() if raise_exceptions: raise e QgsMessageLog.logMessage(str(sys.exc_info()[0]), 'Processing', Qgis.Critical) if feedback is not None: feedback.reportError(getattr(e, 'msg', str(e)), fatalError=True) return False, {}