Skip to content

Commit 0f6838d

Browse files
committedMay 10, 2016
Merge pull request #3034 from rouault/ogr_concurrent_opening
[BUGFIX / FEATURE] [OGR] Allow concurrent edition of Shapefiles and Tabfiles in QGIS & MapInfo
2 parents 7ae80b1 + 52de4fb commit 0f6838d

File tree

10 files changed

+631
-30
lines changed

10 files changed

+631
-30
lines changed
 

‎python/core/qgsdataprovider.sip

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,47 @@ class QgsDataProvider : QObject
223223
*/
224224
virtual void invalidateConnections( const QString& connection );
225225

226+
/** Enter update mode.
227+
*
228+
* This is aimed at providers that can open differently the connection to
229+
* the datasource, according it to be in update mode or in read-only mode.
230+
* A call to this method shall be balanced with a call to leaveUpdateMode(),
231+
* if this method returns true.
232+
*
233+
* Most providers will have an empty implementation for that method.
234+
*
235+
* For backward compatibility, providers that implement enterUpdateMode() should
236+
* still make sure to allow editing operations to work even if enterUpdateMode()
237+
* is not explicitly called.
238+
*
239+
* Several successive calls to enterUpdateMode() can be done. So there is
240+
* a concept of stack of calls that must be handled by the provider. Only the first
241+
* call to enterUpdateMode() will really turn update mode on.
242+
*
243+
* @return true in case of success (or no-op implementation), false in case of failure
244+
*
245+
* @note added in QGIS 2.16
246+
*/
247+
virtual bool enterUpdateMode();
248+
249+
/** Leave update mode.
250+
*
251+
* This is aimed at providers that can open differently the connection to
252+
* the datasource, according it to be in update mode or in read-only mode.
253+
* This method shall be balanced with a succesful call to enterUpdateMode().
254+
*
255+
* Most providers will have an empty implementation for that method.
256+
*
257+
* Several successive calls to enterUpdateMode() can be done. So there is
258+
* a concept of stack of calls that must be handled by the provider. Only the last
259+
* call to leaveUpdateMode() will really turn update mode off.
260+
*
261+
* @return true in case of success (or no-op implementation), false in case of failure
262+
*
263+
* @note added in QGIS 2.16
264+
*/
265+
virtual bool leaveUpdateMode();
266+
226267
signals:
227268

228269
/**

‎src/core/qgsdataprovider.h

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,47 @@ class CORE_EXPORT QgsDataProvider : public QObject
311311
*/
312312
virtual void invalidateConnections( const QString& connection ) { Q_UNUSED( connection ); }
313313

314+
/** Enter update mode.
315+
*
316+
* This is aimed at providers that can open differently the connection to
317+
* the datasource, according it to be in update mode or in read-only mode.
318+
* A call to this method shall be balanced with a call to leaveUpdateMode(),
319+
* if this method returns true.
320+
*
321+
* Most providers will have an empty implementation for that method.
322+
*
323+
* For backward compatibility, providers that implement enterUpdateMode() should
324+
* still make sure to allow editing operations to work even if enterUpdateMode()
325+
* is not explicitly called.
326+
*
327+
* Several successive calls to enterUpdateMode() can be done. So there is
328+
* a concept of stack of calls that must be handled by the provider. Only the first
329+
* call to enterUpdateMode() will really turn update mode on.
330+
*
331+
* @return true in case of success (or no-op implementation), false in case of failure.
332+
*
333+
* @note added in QGIS 2.16
334+
*/
335+
virtual bool enterUpdateMode() { return true; }
336+
337+
/** Leave update mode.
338+
*
339+
* This is aimed at providers that can open differently the connection to
340+
* the datasource, according it to be in update mode or in read-only mode.
341+
* This method shall be balanced with a succesful call to enterUpdateMode().
342+
*
343+
* Most providers will have an empty implementation for that method.
344+
*
345+
* Several successive calls to enterUpdateMode() can be done. So there is
346+
* a concept of stack of calls that must be handled by the provider. Only the last
347+
* call to leaveUpdateMode() will really turn update mode off.
348+
*
349+
* @return true in case of success (or no-op implementation), false in case of failure.
350+
*
351+
* @note added in QGIS 2.16
352+
*/
353+
virtual bool leaveUpdateMode() { return true; }
354+
314355
signals:
315356

316357
/**

‎src/core/qgsvectorlayer.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,7 @@ void QgsVectorLayer::reload()
377377
if ( mDataProvider )
378378
{
379379
mDataProvider->reloadData();
380+
updateFields();
380381
}
381382
}
382383

@@ -1454,6 +1455,8 @@ bool QgsVectorLayer::startEditing()
14541455

14551456
emit beforeEditingStarted();
14561457

1458+
mDataProvider->enterUpdateMode();
1459+
14571460
if ( mDataProvider->transaction() )
14581461
{
14591462
mEditBuffer = new QgsVectorLayerEditPassthrough( this );
@@ -2424,6 +2427,8 @@ bool QgsVectorLayer::commitChanges()
24242427
updateFields();
24252428
mDataProvider->updateExtents();
24262429

2430+
mDataProvider->leaveUpdateMode();
2431+
24272432
emit repaintRequested();
24282433

24292434
return success;
@@ -2474,6 +2479,8 @@ bool QgsVectorLayer::rollBack( bool deleteBuffer )
24742479
if ( rollbackExtent )
24752480
updateExtents();
24762481

2482+
mDataProvider->leaveUpdateMode();
2483+
24772484
emit repaintRequested();
24782485
return true;
24792486
}

‎src/providers/ogr/qgsogrfeatureiterator.cpp

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ QgsOgrFeatureIterator::QgsOgrFeatureIterator( QgsOgrFeatureSource* source, bool
4646
mFeatureFetched = false;
4747

4848
mConn = QgsOgrConnPool::instance()->acquireConnection( mSource->mProvider->dataSourceUri() );
49+
if ( !mConn->ds )
50+
{
51+
return;
52+
}
4953

5054
if ( mSource->mLayerName.isNull() )
5155
{
@@ -55,10 +59,18 @@ QgsOgrFeatureIterator::QgsOgrFeatureIterator( QgsOgrFeatureSource* source, bool
5559
{
5660
ogrLayer = OGR_DS_GetLayerByName( mConn->ds, TO8( mSource->mLayerName ) );
5761
}
62+
if ( !ogrLayer )
63+
{
64+
return;
65+
}
5866

5967
if ( !mSource->mSubsetString.isEmpty() )
6068
{
6169
ogrLayer = QgsOgrProviderUtils::setSubsetString( ogrLayer, mConn->ds, mSource->mEncoding, mSource->mSubsetString );
70+
if ( !ogrLayer )
71+
{
72+
return;
73+
}
6274
mSubsetStringSet = true;
6375
}
6476

@@ -212,7 +224,7 @@ bool QgsOgrFeatureIterator::fetchFeature( QgsFeature& feature )
212224
{
213225
feature.setValid( false );
214226

215-
if ( mClosed )
227+
if ( mClosed || !ogrLayer )
216228
return false;
217229

218230
if ( mRequest.filterType() == QgsFeatureRequest::FilterFid )
@@ -257,7 +269,7 @@ bool QgsOgrFeatureIterator::fetchFeature( QgsFeature& feature )
257269

258270
bool QgsOgrFeatureIterator::rewind()
259271
{
260-
if ( mClosed )
272+
if ( mClosed || !ogrLayer )
261273
return false;
262274

263275
OGR_L_ResetReading( ogrLayer );

‎src/providers/ogr/qgsogrprovider.cpp

Lines changed: 231 additions & 20 deletions
Large diffs are not rendered by default.

‎src/providers/ogr/qgsogrprovider.h

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,9 @@ class QgsOgrProvider : public QgsVectorDataProvider
174174

175175
virtual void setEncoding( const QString& e ) override;
176176

177+
virtual bool enterUpdateMode() override;
178+
179+
virtual bool leaveUpdateMode() override;
177180

178181
/** Return vector file filter string
179182
*
@@ -271,6 +274,9 @@ class QgsOgrProvider : public QgsVectorDataProvider
271274
*/
272275
void forceReload() override;
273276

277+
/** Closes and re-open the datasource */
278+
void reloadData() override;
279+
274280
protected:
275281
/** Loads fields from input file to member attributeFields */
276282
void loadFields();
@@ -287,7 +293,15 @@ class QgsOgrProvider : public QgsVectorDataProvider
287293
/** Clean shapefile from features which are marked as deleted */
288294
void repack();
289295

290-
void open();
296+
enum OpenMode
297+
{
298+
OpenModeInitial,
299+
OpenModeSameAsCurrent,
300+
OpenModeForceReadOnly,
301+
OpenModeForceUpdate,
302+
};
303+
304+
void open( OpenMode mode );
291305
void close();
292306

293307
private:
@@ -356,10 +370,24 @@ class QgsOgrProvider : public QgsVectorDataProvider
356370
/** Whether the file is opened in write mode*/
357371
bool mWriteAccess;
358372

373+
/** Whether the file can potentially be opened in write mode (but not necessarily currently) */
374+
bool mWriteAccessPossible;
375+
376+
/** Whether the open mode of the datasource changes w.r.t calls to enterUpdateMode() / leaveUpdateMode() */
377+
bool mDynamicWriteAccess;
378+
359379
bool mShapefileMayBeCorrupted;
360380

361381
/** Converts the geometry to the layer type if necessary. Takes ownership of the passed geometry */
362382
OGRGeometryH ConvertGeometryIfNecessary( OGRGeometryH );
383+
384+
int mUpdateModeStackDepth;
385+
386+
void computeCapabilities();
387+
388+
int mCapabilities;
389+
390+
bool doInitialActionsForEdition();
363391
};
364392

365393

‎tests/src/python/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ ADD_PYTHON_TEST(PyQgsRulebasedRenderer test_qgsrulebasedrenderer.py)
6464
ADD_PYTHON_TEST(PyQgsSingleSymbolRenderer test_qgssinglesymbolrenderer.py)
6565
ADD_PYTHON_TEST(PyQgsShapefileProvider test_provider_shapefile.py)
6666
ADD_PYTHON_TEST(PyQgsTabfileProvider test_provider_tabfile.py)
67+
ADD_PYTHON_TEST(PyQgsOGRProvider test_provider_ogr.py)
6768
ADD_PYTHON_TEST(PyQgsSpatialIndex test_qgsspatialindex.py)
6869
ADD_PYTHON_TEST(PyQgsSpatialiteProvider test_provider_spatialite.py)
6970
ADD_PYTHON_TEST(PyQgsStringStatisticalSummary test_qgsstringstatisticalsummary.py)

‎tests/src/python/test_provider_ogr.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# -*- coding: utf-8 -*-
2+
"""QGIS Unit tests for the non-shapefile, non-tabfile datasources handled by OGR provider.
3+
4+
.. note:: This program is free software; you can redistribute it and/or modify
5+
it under the terms of the GNU General Public License as published by
6+
the Free Software Foundation; either version 2 of the License, or
7+
(at your option) any later version.
8+
"""
9+
__author__ = 'Even Rouault'
10+
__date__ = '2016-04-11'
11+
__copyright__ = 'Copyright 2016, Even Rouault'
12+
# This will get replaced with a git SHA1 when you do a git archive
13+
__revision__ = '$Format:%H$'
14+
15+
import os
16+
import shutil
17+
import tempfile
18+
19+
from qgis.core import QgsVectorLayer, QgsVectorDataProvider
20+
from qgis.testing import (
21+
start_app,
22+
unittest
23+
)
24+
from utilities import unitTestDataPath
25+
26+
start_app()
27+
TEST_DATA_DIR = unitTestDataPath()
28+
29+
# Note - doesn't implement ProviderTestCase as most OGR provider is tested by the shapefile provider test
30+
31+
32+
class PyQgsOGRProvider(unittest.TestCase):
33+
34+
@classmethod
35+
def setUpClass(cls):
36+
"""Run before all tests"""
37+
# Create test layer
38+
cls.basetestpath = tempfile.mkdtemp()
39+
cls.datasource = os.path.join(cls.basetestpath, 'test.csv')
40+
with open(cls.datasource, 'wt') as f:
41+
f.write('id,WKT\n')
42+
f.write('1,POINT(2 49)\n')
43+
44+
cls.dirs_to_cleanup = [cls.basetestpath]
45+
46+
@classmethod
47+
def tearDownClass(cls):
48+
"""Run after all tests"""
49+
for dirname in cls.dirs_to_cleanup:
50+
shutil.rmtree(dirname, True)
51+
52+
def testUpdateMode(self):
53+
54+
vl = QgsVectorLayer(u'{}|layerid=0'.format(self.datasource), u'test', u'ogr')
55+
self.assertTrue(vl.isValid())
56+
caps = vl.dataProvider().capabilities()
57+
self.assertTrue(caps & QgsVectorDataProvider.AddFeatures)
58+
59+
self.assertEqual(vl.dataProvider().property("_debug_open_mode"), "read-write")
60+
61+
# No-op
62+
self.assertTrue(vl.dataProvider().enterUpdateMode())
63+
self.assertEqual(vl.dataProvider().property("_debug_open_mode"), "read-write")
64+
65+
# No-op
66+
self.assertTrue(vl.dataProvider().leaveUpdateMode())
67+
self.assertEqual(vl.dataProvider().property("_debug_open_mode"), "read-write")
68+
69+
70+
if __name__ == '__main__':
71+
unittest.main()

‎tests/src/python/test_provider_shapefile.py

Lines changed: 172 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,25 @@
1212
# This will get replaced with a git SHA1 when you do a git archive
1313
__revision__ = '$Format:%H$'
1414

15-
import qgis # NOQA
16-
1715
import os
1816
import tempfile
1917
import shutil
2018
import glob
2119
import osgeo.gdal
20+
import sys
2221

23-
from qgis.core import QgsVectorLayer, QgsFeatureRequest
24-
from qgis.PyQt.QtCore import QSettings
22+
from qgis.core import QgsFeature, QgsField, QgsGeometry, QgsVectorLayer, QgsFeatureRequest, QgsVectorDataProvider
23+
from qgis.PyQt.QtCore import QSettings, QVariant
2524
from qgis.testing import start_app, unittest
2625
from utilities import unitTestDataPath
2726
from providertestbase import ProviderTestCase
2827

28+
try:
29+
from osgeo import gdal
30+
gdal_ok = True
31+
except:
32+
gdal_ok = False
33+
2934
start_app()
3035
TEST_DATA_DIR = unitTestDataPath()
3136

@@ -55,11 +60,13 @@ def setUpClass(cls):
5560
assert (cls.vl_poly.isValid())
5661
cls.poly_provider = cls.vl_poly.dataProvider()
5762

63+
cls.dirs_to_cleanup = [cls.basetestpath, cls.repackfilepath]
64+
5865
@classmethod
5966
def tearDownClass(cls):
6067
"""Run after all tests"""
61-
shutil.rmtree(cls.basetestpath, True)
62-
shutil.rmtree(cls.repackfilepath, True)
68+
for dirname in cls.dirs_to_cleanup:
69+
shutil.rmtree(dirname, True)
6370

6471
def enableCompiler(self):
6572
QSettings().setValue(u'/qgis/compileExpressions', True)
@@ -128,6 +135,165 @@ def testRepack(self):
128135
self.assertTrue(vl.commitChanges())
129136
self.assertTrue(vl.selectedFeatureCount() == 0 or vl.selectedFeatures()[0]['pk'] == 1)
130137

138+
def testUpdateMode(self):
139+
""" Test that on-the-fly re-opening in update/read-only mode works """
140+
141+
tmpdir = tempfile.mkdtemp()
142+
self.dirs_to_cleanup.append(tmpdir)
143+
srcpath = os.path.join(TEST_DATA_DIR, 'provider')
144+
for file in glob.glob(os.path.join(srcpath, 'shapefile.*')):
145+
shutil.copy(os.path.join(srcpath, file), tmpdir)
146+
datasource = os.path.join(tmpdir, 'shapefile.shp')
147+
148+
vl = QgsVectorLayer(u'{}|layerid=0'.format(datasource), u'test', u'ogr')
149+
caps = vl.dataProvider().capabilities()
150+
self.assertTrue(caps & QgsVectorDataProvider.AddFeatures)
151+
self.assertTrue(caps & QgsVectorDataProvider.DeleteFeatures)
152+
self.assertTrue(caps & QgsVectorDataProvider.ChangeAttributeValues)
153+
self.assertTrue(caps & QgsVectorDataProvider.AddAttributes)
154+
self.assertTrue(caps & QgsVectorDataProvider.DeleteAttributes)
155+
self.assertTrue(caps & QgsVectorDataProvider.CreateSpatialIndex)
156+
self.assertTrue(caps & QgsVectorDataProvider.SelectAtId)
157+
self.assertTrue(caps & QgsVectorDataProvider.ChangeGeometries)
158+
self.assertTrue(caps & QgsVectorDataProvider.SimplifyGeometries)
159+
self.assertTrue(caps & QgsVectorDataProvider.SimplifyGeometriesWithTopologicalValidation)
160+
#self.assertTrue(caps & QgsVectorDataProvider.ChangeFeatures)
161+
162+
# We should be really opened in read-only mode even if write capabilities are declared
163+
self.assertEquals(vl.dataProvider().property("_debug_open_mode"), "read-only")
164+
165+
# Unbalanced call to leaveUpdateMode()
166+
self.assertFalse(vl.dataProvider().leaveUpdateMode())
167+
168+
# Test that startEditing() / commitChanges() plays with enterUpdateMode() / leaveUpdateMode()
169+
self.assertTrue(vl.startEditing())
170+
self.assertEquals(vl.dataProvider().property("_debug_open_mode"), "read-write")
171+
self.assertTrue(vl.dataProvider().isValid())
172+
173+
self.assertTrue(vl.commitChanges())
174+
self.assertEquals(vl.dataProvider().property("_debug_open_mode"), "read-only")
175+
self.assertTrue(vl.dataProvider().isValid())
176+
177+
# Manual enterUpdateMode() / leaveUpdateMode() with 2 depths
178+
self.assertTrue(vl.dataProvider().enterUpdateMode())
179+
self.assertEquals(vl.dataProvider().property("_debug_open_mode"), "read-write")
180+
caps = vl.dataProvider().capabilities()
181+
self.assertTrue(caps & QgsVectorDataProvider.AddFeatures)
182+
183+
f = QgsFeature()
184+
f.setAttributes([200])
185+
f.setGeometry(QgsGeometry.fromWkt('Point (2 49)'))
186+
(ret, feature_list) = vl.dataProvider().addFeatures([f])
187+
self.assertTrue(ret)
188+
fid = feature_list[0].id()
189+
190+
features = [f_iter for f_iter in vl.getFeatures(QgsFeatureRequest().setFilterFid(fid))]
191+
values = [f_iter['pk'] for f_iter in features]
192+
self.assertEquals(values, [200])
193+
194+
got_geom = [f_iter.geometry() for f_iter in features][0].geometry()
195+
self.assertEquals((got_geom.x(), got_geom.y()), (2.0, 49.0))
196+
197+
self.assertTrue(vl.dataProvider().changeGeometryValues({fid: QgsGeometry.fromWkt('Point (3 50)')}))
198+
self.assertTrue(vl.dataProvider().changeAttributeValues({fid: {0: 100}}))
199+
200+
features = [f_iter for f_iter in vl.getFeatures(QgsFeatureRequest().setFilterFid(fid))]
201+
values = [f_iter['pk'] for f_iter in features]
202+
203+
got_geom = [f_iter.geometry() for f_iter in features][0].geometry()
204+
self.assertEquals((got_geom.x(), got_geom.y()), (3.0, 50.0))
205+
206+
self.assertTrue(vl.dataProvider().deleteFeatures([fid]))
207+
208+
# Check that it has really disappeared
209+
if gdal_ok:
210+
gdal.PushErrorHandler('CPLQuietErrorHandler')
211+
features = [f_iter for f_iter in vl.getFeatures(QgsFeatureRequest().setFilterFid(fid))]
212+
if gdal_ok:
213+
gdal.PopErrorHandler()
214+
self.assertEquals(features, [])
215+
216+
self.assertTrue(vl.dataProvider().addAttributes([QgsField("new_field", QVariant.Int, "integer")]))
217+
self.assertTrue(vl.dataProvider().deleteAttributes([len(vl.dataProvider().fields()) - 1]))
218+
219+
self.assertTrue(vl.startEditing())
220+
self.assertEquals(vl.dataProvider().property("_debug_open_mode"), "read-write")
221+
222+
self.assertTrue(vl.commitChanges())
223+
self.assertEquals(vl.dataProvider().property("_debug_open_mode"), "read-write")
224+
225+
self.assertTrue(vl.dataProvider().enterUpdateMode())
226+
self.assertEquals(vl.dataProvider().property("_debug_open_mode"), "read-write")
227+
228+
self.assertTrue(vl.dataProvider().leaveUpdateMode())
229+
self.assertEquals(vl.dataProvider().property("_debug_open_mode"), "read-write")
230+
231+
self.assertTrue(vl.dataProvider().leaveUpdateMode())
232+
self.assertEquals(vl.dataProvider().property("_debug_open_mode"), "read-only")
233+
234+
# Test that update mode will be implictly enabled if doing an action
235+
# that requires update mode
236+
(ret, _) = vl.dataProvider().addFeatures([QgsFeature()])
237+
self.assertTrue(ret)
238+
self.assertEquals(vl.dataProvider().property("_debug_open_mode"), "read-write")
239+
240+
def testUpdateModeFailedReopening(self):
241+
''' Test that methods on provider don't crash after a failed reopening '''
242+
243+
# Windows doesn't like removing files opened by OGR, whatever
244+
# their open mode, so that makes it hard to test
245+
if sys.platform == 'win32':
246+
return
247+
248+
tmpdir = tempfile.mkdtemp()
249+
self.dirs_to_cleanup.append(tmpdir)
250+
srcpath = os.path.join(TEST_DATA_DIR, 'provider')
251+
for file in glob.glob(os.path.join(srcpath, 'shapefile.*')):
252+
shutil.copy(os.path.join(srcpath, file), tmpdir)
253+
datasource = os.path.join(tmpdir, 'shapefile.shp')
254+
255+
vl = QgsVectorLayer(u'{}|layerid=0'.format(datasource), u'test', u'ogr')
256+
257+
os.unlink(datasource)
258+
259+
self.assertFalse(vl.dataProvider().enterUpdateMode())
260+
self.assertFalse(vl.dataProvider().enterUpdateMode())
261+
self.assertEquals(vl.dataProvider().property("_debug_open_mode"), "invalid")
262+
263+
self.assertFalse(vl.dataProvider().isValid())
264+
self.assertEquals(len([f for f in vl.dataProvider().getFeatures()]), 0)
265+
self.assertEquals(len(vl.dataProvider().subLayers()), 0)
266+
self.assertFalse(vl.dataProvider().setSubsetString('TRUE'))
267+
(ret, _) = vl.dataProvider().addFeatures([QgsFeature()])
268+
self.assertFalse(ret)
269+
self.assertFalse(vl.dataProvider().deleteFeatures([1]))
270+
self.assertFalse(vl.dataProvider().addAttributes([QgsField()]))
271+
self.assertFalse(vl.dataProvider().deleteAttributes([1]))
272+
self.assertFalse(vl.dataProvider().changeGeometryValues({0: QgsGeometry.fromWkt('Point (3 50)')}))
273+
self.assertFalse(vl.dataProvider().changeAttributeValues({0: {0: 0}}))
274+
self.assertFalse(vl.dataProvider().createSpatialIndex())
275+
self.assertFalse(vl.dataProvider().createAttributeIndex(0))
276+
277+
def testreloadData(self):
278+
''' Test reloadData() '''
279+
280+
tmpdir = tempfile.mkdtemp()
281+
self.dirs_to_cleanup.append(tmpdir)
282+
srcpath = os.path.join(TEST_DATA_DIR, 'provider')
283+
for file in glob.glob(os.path.join(srcpath, 'shapefile.*')):
284+
shutil.copy(os.path.join(srcpath, file), tmpdir)
285+
datasource = os.path.join(tmpdir, 'shapefile.shp')
286+
287+
vl1 = QgsVectorLayer(u'{}|layerid=0'.format(datasource), u'test', u'ogr')
288+
vl2 = QgsVectorLayer(u'{}|layerid=0'.format(datasource), u'test', u'ogr')
289+
self.assertTrue(vl1.startEditing())
290+
self.assertTrue(vl1.deleteAttributes([1]))
291+
self.assertTrue(vl1.commitChanges())
292+
self.assertEquals(len(vl1.fields()) + 1, len(vl2.fields()))
293+
# Reload
294+
vl2.reload()
295+
# And now check that fields are up-to-date
296+
self.assertEquals(len(vl1.fields()), len(vl2.fields()))
131297

132298
if __name__ == '__main__':
133299
unittest.main()

‎tests/src/python/test_provider_tabfile.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,13 @@
1414

1515
import os
1616

17-
from qgis.core import QgsVectorLayer, QgsFeatureRequest
17+
from qgis.core import QgsVectorLayer, QgsFeatureRequest, QgsVectorDataProvider
1818
from qgis.PyQt.QtCore import QDate, QTime, QDateTime, QVariant
1919
from qgis.testing import (
2020
start_app,
2121
unittest
2222
)
23+
import osgeo.gdal
2324
from utilities import unitTestDataPath
2425

2526
start_app()
@@ -52,5 +53,27 @@ def testDateTimeFormats(self):
5253
assert isinstance(f.attributes()[datetime_idx], QDateTime)
5354
self.assertEqual(f.attributes()[datetime_idx], QDateTime(QDate(2004, 5, 3), QTime(13, 41, 00)))
5455

56+
# This test fails with GDAL version < 2 because tab update is new in GDAL 2.0
57+
@unittest.expectedFailure(int(osgeo.gdal.VersionInfo()[:1]) < 2)
58+
def testUpdateMode(self):
59+
""" Test that on-the-fly re-opening in update/read-only mode works """
60+
61+
basetestfile = os.path.join(TEST_DATA_DIR, 'tab_file.tab')
62+
vl = QgsVectorLayer(u'{}|layerid=0'.format(basetestfile), u'test', u'ogr')
63+
caps = vl.dataProvider().capabilities()
64+
self.assertTrue(caps & QgsVectorDataProvider.AddFeatures)
65+
66+
# We should be really opened in read-only mode even if write capabilities are declared
67+
self.assertEquals(vl.dataProvider().property("_debug_open_mode"), "read-only")
68+
69+
# Test that startEditing() / commitChanges() plays with enterUpdateMode() / leaveUpdateMode()
70+
self.assertTrue(vl.startEditing())
71+
self.assertEquals(vl.dataProvider().property("_debug_open_mode"), "read-write")
72+
self.assertTrue(vl.dataProvider().isValid())
73+
74+
self.assertTrue(vl.commitChanges())
75+
self.assertEquals(vl.dataProvider().property("_debug_open_mode"), "read-only")
76+
self.assertTrue(vl.dataProvider().isValid())
77+
5578
if __name__ == '__main__':
5679
unittest.main()

0 commit comments

Comments
 (0)
Please sign in to comment.