Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 94412d1

Browse files
committedJul 22, 2019
[API][FEATURE] Allow setting a custom path pre-processor for QgsPathResolver
QgsPathResolver::setPathPreprocessor allows setting a custom path pre-processor function, which allows for manipulation of paths and data sources prior to resolving them to file references or layer sources. The processor function must accept a single string argument (representing the original file path or data source), and return a processed version of this path. The path pre-processor function is called before any bad layer handler. Example - replace an outdated folder path with a new one: def my_processor(path): return path.replace('c:/Users/ClintBarton/Documents/Projects', 'x:/Projects/') QgsPathResolver.setPathPreprocessor(my_processor) Example - replace a stored database host with a new one: def my_processor(path): return path.replace('host=10.1.1.115', 'host=10.1.1.116') QgsPathResolver.setPathPreprocessor(my_processor) Example - replace stored database credentials with new ones: def my_processor(path): path= path.replace("user='gis_team'", "user='team_awesome'") path = path.replace("password='cats'", "password='g7as!m*'") return path QgsPathResolver.setPathPreprocessor(my_processor)
1 parent 859d9a7 commit 94412d1

File tree

5 files changed

+241
-3
lines changed

5 files changed

+241
-3
lines changed
 

‎python/core/auto_generated/qgspathresolver.sip.in

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99

1010

1111

12-
1312
class QgsPathResolver
1413
{
1514
%Docstring
@@ -39,6 +38,75 @@ Paths written to the project file should be prepared with this method.
3938
Turn filename read from the project file to an absolute path
4039
%End
4140

41+
42+
static void setPathPreprocessor( SIP_PYCALLABLE / AllowNone / );
43+
%Docstring
44+
Sets a path pre-processor function, which allows for manipulation of paths and data sources prior
45+
to resolving them to file references or layer sources.
46+
47+
The ``processor`` function must accept a single string argument (representing the original file path
48+
or data source), and return a processed version of this path.
49+
50+
The path pre-processor function is called before any bad layer handler.
51+
52+
.. note::
53+
54+
Setting a new ``processor`` replaces any existing processor.
55+
56+
Example - replace an outdated folder path with a new one:
57+
.. code-block:: python
58+
59+
def my_processor(path):
60+
return path.replace('c:/Users/ClintBarton/Documents/Projects', 'x:/Projects/')
61+
62+
QgsPathResolver.setPathPreprocessor(my_processor)
63+
64+
Example - replace a stored database host with a new one:
65+
.. code-block:: python
66+
67+
def my_processor(path):
68+
return path.replace('host=10.1.1.115', 'host=10.1.1.116')
69+
70+
QgsPathResolver.setPathPreprocessor(my_processor)
71+
72+
73+
Example - replace stored database credentials with new ones:
74+
.. code-block:: python
75+
76+
def my_processor(path):
77+
path = path.replace("user='gis_team'", "user='team_awesome'")
78+
path = path.replace("password='cats'", "password='g7as!m*'")
79+
return path
80+
81+
QgsPathResolver.setPathPreprocessor(my_processor)
82+
83+
84+
85+
.. versionadded:: 3.10
86+
%End
87+
%MethodCode
88+
Py_BEGIN_ALLOW_THREADS
89+
90+
QgsPathResolver::setPathPreprocessor( [a0]( const QString &arg )->QString
91+
{
92+
QString res;
93+
SIP_BLOCK_THREADS
94+
PyObject *s = sipCallMethod( NULL, a0, "D", &arg, sipType_QString, NULL );
95+
int state;
96+
int sipIsError = 0;
97+
QString *t1 = reinterpret_cast<QString *>( sipConvertToType( s, sipType_QString, 0, SIP_NOT_NONE, &state, &sipIsError ) );
98+
if ( sipIsError == 0 )
99+
{
100+
res = QString( *t1 );
101+
}
102+
sipReleaseType( t1, sipType_QString, state );
103+
SIP_UNBLOCK_THREADS
104+
return res;
105+
} );
106+
107+
Py_END_ALLOW_THREADS
108+
%End
109+
42110
};
43111

44112
/************************************************************************

‎src/core/qgspathresolver.cpp

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,17 @@
2020
#include <QFileInfo>
2121
#include <QUrl>
2222

23+
std::function< QString( const QString & ) > QgsPathResolver::sCustomResolver = []( const QString &a )->QString { return a; };
2324

2425
QgsPathResolver::QgsPathResolver( const QString &baseFileName )
2526
: mBaseFileName( baseFileName )
2627
{
2728
}
2829

2930

30-
QString QgsPathResolver::readPath( const QString &filename ) const
31+
QString QgsPathResolver::readPath( const QString &f ) const
3132
{
33+
QString filename = sCustomResolver( f );
3234
if ( filename.isEmpty() )
3335
return QString();
3436

@@ -142,6 +144,11 @@ QString QgsPathResolver::readPath( const QString &filename ) const
142144
return vsiPrefix + projElems.join( QStringLiteral( "/" ) );
143145
}
144146

147+
void QgsPathResolver::setPathPreprocessor( const std::function<QString( const QString & )> &processor )
148+
{
149+
sCustomResolver = processor;
150+
}
151+
145152
QString QgsPathResolver::writePath( const QString &src ) const
146153
{
147154
if ( mBaseFileName.isEmpty() || src.isEmpty() )

‎src/core/qgspathresolver.h

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
#include "qgis_core.h"
2020

2121
#include <QString>
22-
22+
#include <functional>
2323

2424
/**
2525
* \ingroup core
@@ -43,9 +43,93 @@ class CORE_EXPORT QgsPathResolver
4343
//! Turn filename read from the project file to an absolute path
4444
QString readPath( const QString &filename ) const;
4545

46+
#ifndef SIP_RUN
47+
48+
/**
49+
* Sets a path pre-processor function, which allows for manipulation of paths and data sources prior
50+
* to resolving them to file references or layer sources.
51+
*
52+
* The \a processor function must accept a single string argument (representing the original file path
53+
* or data source), and return a processed version of this path.
54+
*
55+
* The path pre-processor function is called before any bad layer handler.
56+
*
57+
* \note Setting a new \a processor replaces any existing processor.
58+
*
59+
* \since QGIS 3.10
60+
*/
61+
static void setPathPreprocessor( const std::function< QString( const QString &filename )> &processor );
62+
#else
63+
64+
/**
65+
* Sets a path pre-processor function, which allows for manipulation of paths and data sources prior
66+
* to resolving them to file references or layer sources.
67+
*
68+
* The \a processor function must accept a single string argument (representing the original file path
69+
* or data source), and return a processed version of this path.
70+
*
71+
* The path pre-processor function is called before any bad layer handler.
72+
*
73+
* \note Setting a new \a processor replaces any existing processor.
74+
*
75+
* Example - replace an outdated folder path with a new one:
76+
* \code{.py}
77+
* def my_processor(path):
78+
* return path.replace('c:/Users/ClintBarton/Documents/Projects', 'x:/Projects/')
79+
*
80+
* QgsPathResolver.setPathPreprocessor(my_processor)
81+
* \endcode
82+
*
83+
* Example - replace a stored database host with a new one:
84+
* \code{.py}
85+
* def my_processor(path):
86+
* return path.replace('host=10.1.1.115', 'host=10.1.1.116')
87+
*
88+
* QgsPathResolver.setPathPreprocessor(my_processor)
89+
* \endcode
90+
*
91+
* Example - replace stored database credentials with new ones:
92+
* \code{.py}
93+
* def my_processor(path):
94+
* path = path.replace("user='gis_team'", "user='team_awesome'")
95+
* path = path.replace("password='cats'", "password='g7as!m*'")
96+
* return path
97+
*
98+
* QgsPathResolver.setPathPreprocessor(my_processor)
99+
* \endcode
100+
*
101+
* \since QGIS 3.10
102+
*/
103+
static void setPathPreprocessor( SIP_PYCALLABLE / AllowNone / );
104+
% MethodCode
105+
Py_BEGIN_ALLOW_THREADS
106+
107+
QgsPathResolver::setPathPreprocessor( [a0]( const QString &arg )->QString
108+
{
109+
QString res;
110+
SIP_BLOCK_THREADS
111+
PyObject *s = sipCallMethod( NULL, a0, "D", &arg, sipType_QString, NULL );
112+
int state;
113+
int sipIsError = 0;
114+
QString *t1 = reinterpret_cast<QString *>( sipConvertToType( s, sipType_QString, 0, SIP_NOT_NONE, &state, &sipIsError ) );
115+
if ( sipIsError == 0 )
116+
{
117+
res = QString( *t1 );
118+
}
119+
sipReleaseType( t1, sipType_QString, state );
120+
SIP_UNBLOCK_THREADS
121+
return res;
122+
} );
123+
124+
Py_END_ALLOW_THREADS
125+
% End
126+
#endif
127+
46128
private:
47129
//! path to a file that is the base for relative path resolution
48130
QString mBaseFileName;
131+
132+
static std::function< QString( const QString & ) > sCustomResolver;
49133
};
50134

51135
#endif // QGSPATHRESOLVER_H

‎tests/src/python/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ ADD_PYTHON_TEST(PyQgsPalLabelingLayout test_qgspallabeling_layout.py)
159159
ADD_PYTHON_TEST(PyQgsPalLabelingPlacement test_qgspallabeling_placement.py)
160160
ADD_PYTHON_TEST(PyQgsPanelWidget test_qgspanelwidget.py)
161161
ADD_PYTHON_TEST(PyQgsPanelWidgetStack test_qgspanelwidgetstack.py)
162+
ADD_PYTHON_TEST(PyQgsPathResolver test_qgspathresolver.py)
162163
ADD_PYTHON_TEST(PyQgsPoint test_qgspoint.py)
163164
ADD_PYTHON_TEST(PyQgsPointClusterRenderer test_qgspointclusterrenderer.py)
164165
ADD_PYTHON_TEST(PyQgsPointDisplacementRenderer test_qgspointdisplacementrenderer.py)
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# -*- coding: utf-8 -*-
2+
"""QGIS Unit tests for QgsPathResolver.
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__ = 'Nyall Dawson'
10+
__date__ = '22/07/2019'
11+
__copyright__ = 'Copyright 2019, The QGIS Project'
12+
13+
import qgis # NOQA
14+
15+
import tempfile
16+
import os
17+
from qgis.core import QgsPathResolver, QgsVectorLayer, QgsProject
18+
from qgis.testing import start_app, unittest
19+
from utilities import unitTestDataPath
20+
21+
start_app()
22+
23+
TEST_DATA_DIR = unitTestDataPath()
24+
25+
PROCESSOR = None
26+
27+
28+
class TestQgsPathResolver(unittest.TestCase):
29+
30+
def testCustomPreprocessor(self):
31+
self.assertEqual(QgsPathResolver().readPath('aaaaa'), 'aaaaa')
32+
33+
def my_processor(path):
34+
return path.upper()
35+
36+
global PROCESSOR
37+
PROCESSOR = my_processor
38+
QgsPathResolver.setPathPreprocessor(PROCESSOR)
39+
self.assertEqual(QgsPathResolver().readPath('aaaaa'), 'AAAAA')
40+
41+
def testLoadLayerWithPreprocessor(self):
42+
"""
43+
Test that custom path preprocessor is used when loading layers
44+
"""
45+
lines_shp_path = os.path.join(TEST_DATA_DIR, 'moooooo.shp')
46+
47+
lines_layer = QgsVectorLayer(lines_shp_path, 'Lines', 'ogr')
48+
self.assertFalse(lines_layer.isValid())
49+
p = QgsProject()
50+
p.addMapLayer(lines_layer)
51+
# save project to a temporary file
52+
temp_path = tempfile.mkdtemp()
53+
temp_project_path = os.path.join(temp_path, 'temp.qgs')
54+
self.assertTrue(p.write(temp_project_path))
55+
56+
p2 = QgsProject()
57+
self.assertTrue(p2.read(temp_project_path))
58+
l = p2.mapLayersByName('Lines')[0]
59+
self.assertEqual(l.name(), 'Lines')
60+
self.assertFalse(l.isValid())
61+
62+
# custom processor to fix path
63+
def my_processor(path):
64+
return path.replace('moooooo', 'lines')
65+
66+
global PROCESSOR
67+
PROCESSOR = my_processor
68+
QgsPathResolver.setPathPreprocessor(PROCESSOR)
69+
p3 = QgsProject()
70+
self.assertTrue(p3.read(temp_project_path))
71+
l = p3.mapLayersByName('Lines')[0]
72+
self.assertEqual(l.name(), 'Lines')
73+
# layer should have correct path now
74+
self.assertTrue(l.isValid())
75+
76+
77+
if __name__ == '__main__':
78+
unittest.main()

0 commit comments

Comments
 (0)
Please sign in to comment.