Skip to content

Commit

Permalink
[API][FEATURE] Allow setting a custom path pre-processor for QgsPathR…
Browse files Browse the repository at this point in the history
…esolver

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)
  • Loading branch information
nyalldawson committed Jul 22, 2019
1 parent 859d9a7 commit 94412d1
Show file tree
Hide file tree
Showing 5 changed files with 241 additions and 3 deletions.
70 changes: 69 additions & 1 deletion python/core/auto_generated/qgspathresolver.sip.in
Expand Up @@ -9,7 +9,6 @@




class QgsPathResolver
{
%Docstring
Expand Down Expand Up @@ -39,6 +38,75 @@ Paths written to the project file should be prepared with this method.
Turn filename read from the project file to an absolute path
%End


static void setPathPreprocessor( SIP_PYCALLABLE / AllowNone / );
%Docstring
Sets a 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.

.. note::

Setting a new ``processor`` replaces any existing processor.

Example - replace an outdated folder path with a new one:
.. code-block:: python

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:
.. code-block:: python

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:
.. code-block:: python

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)



.. versionadded:: 3.10
%End
%MethodCode
Py_BEGIN_ALLOW_THREADS

QgsPathResolver::setPathPreprocessor( [a0]( const QString &arg )->QString
{
QString res;
SIP_BLOCK_THREADS
PyObject *s = sipCallMethod( NULL, a0, "D", &arg, sipType_QString, NULL );
int state;
int sipIsError = 0;
QString *t1 = reinterpret_cast<QString *>( sipConvertToType( s, sipType_QString, 0, SIP_NOT_NONE, &state, &sipIsError ) );
if ( sipIsError == 0 )
{
res = QString( *t1 );
}
sipReleaseType( t1, sipType_QString, state );
SIP_UNBLOCK_THREADS
return res;
} );

Py_END_ALLOW_THREADS
%End

};

/************************************************************************
Expand Down
9 changes: 8 additions & 1 deletion src/core/qgspathresolver.cpp
Expand Up @@ -20,15 +20,17 @@
#include <QFileInfo>
#include <QUrl>

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

QgsPathResolver::QgsPathResolver( const QString &baseFileName )
: mBaseFileName( baseFileName )
{
}


QString QgsPathResolver::readPath( const QString &filename ) const
QString QgsPathResolver::readPath( const QString &f ) const
{
QString filename = sCustomResolver( f );
if ( filename.isEmpty() )
return QString();

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

void QgsPathResolver::setPathPreprocessor( const std::function<QString( const QString & )> &processor )
{
sCustomResolver = processor;
}

QString QgsPathResolver::writePath( const QString &src ) const
{
if ( mBaseFileName.isEmpty() || src.isEmpty() )
Expand Down
86 changes: 85 additions & 1 deletion src/core/qgspathresolver.h
Expand Up @@ -19,7 +19,7 @@
#include "qgis_core.h"

#include <QString>

#include <functional>

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

#ifndef SIP_RUN

/**
* Sets a path pre-processor function, which allows for manipulation of paths and data sources prior
* to resolving them to file references or layer sources.
*
* The \a 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.
*
* \note Setting a new \a processor replaces any existing processor.
*
* \since QGIS 3.10
*/
static void setPathPreprocessor( const std::function< QString( const QString &filename )> &processor );
#else

/**
* Sets a path pre-processor function, which allows for manipulation of paths and data sources prior
* to resolving them to file references or layer sources.
*
* The \a 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.
*
* \note Setting a new \a processor replaces any existing processor.
*
* Example - replace an outdated folder path with a new one:
* \code{.py}
* def my_processor(path):
* return path.replace('c:/Users/ClintBarton/Documents/Projects', 'x:/Projects/')
*
* QgsPathResolver.setPathPreprocessor(my_processor)
* \endcode
*
* Example - replace a stored database host with a new one:
* \code{.py}
* def my_processor(path):
* return path.replace('host=10.1.1.115', 'host=10.1.1.116')
*
* QgsPathResolver.setPathPreprocessor(my_processor)
* \endcode
*
* Example - replace stored database credentials with new ones:
* \code{.py}
* 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)
* \endcode
*
* \since QGIS 3.10
*/
static void setPathPreprocessor( SIP_PYCALLABLE / AllowNone / );
% MethodCode
Py_BEGIN_ALLOW_THREADS

QgsPathResolver::setPathPreprocessor( [a0]( const QString &arg )->QString
{
QString res;
SIP_BLOCK_THREADS
PyObject *s = sipCallMethod( NULL, a0, "D", &arg, sipType_QString, NULL );
int state;
int sipIsError = 0;
QString *t1 = reinterpret_cast<QString *>( sipConvertToType( s, sipType_QString, 0, SIP_NOT_NONE, &state, &sipIsError ) );
if ( sipIsError == 0 )
{
res = QString( *t1 );
}
sipReleaseType( t1, sipType_QString, state );
SIP_UNBLOCK_THREADS
return res;
} );

Py_END_ALLOW_THREADS
% End
#endif

private:
//! path to a file that is the base for relative path resolution
QString mBaseFileName;

static std::function< QString( const QString & ) > sCustomResolver;
};

#endif // QGSPATHRESOLVER_H
1 change: 1 addition & 0 deletions tests/src/python/CMakeLists.txt
Expand Up @@ -159,6 +159,7 @@ ADD_PYTHON_TEST(PyQgsPalLabelingLayout test_qgspallabeling_layout.py)
ADD_PYTHON_TEST(PyQgsPalLabelingPlacement test_qgspallabeling_placement.py)
ADD_PYTHON_TEST(PyQgsPanelWidget test_qgspanelwidget.py)
ADD_PYTHON_TEST(PyQgsPanelWidgetStack test_qgspanelwidgetstack.py)
ADD_PYTHON_TEST(PyQgsPathResolver test_qgspathresolver.py)
ADD_PYTHON_TEST(PyQgsPoint test_qgspoint.py)
ADD_PYTHON_TEST(PyQgsPointClusterRenderer test_qgspointclusterrenderer.py)
ADD_PYTHON_TEST(PyQgsPointDisplacementRenderer test_qgspointdisplacementrenderer.py)
Expand Down
78 changes: 78 additions & 0 deletions tests/src/python/test_qgspathresolver.py
@@ -0,0 +1,78 @@
# -*- coding: utf-8 -*-
"""QGIS Unit tests for QgsPathResolver.
.. note:: 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.
"""
__author__ = 'Nyall Dawson'
__date__ = '22/07/2019'
__copyright__ = 'Copyright 2019, The QGIS Project'

import qgis # NOQA

import tempfile
import os
from qgis.core import QgsPathResolver, QgsVectorLayer, QgsProject
from qgis.testing import start_app, unittest
from utilities import unitTestDataPath

start_app()

TEST_DATA_DIR = unitTestDataPath()

PROCESSOR = None


class TestQgsPathResolver(unittest.TestCase):

def testCustomPreprocessor(self):
self.assertEqual(QgsPathResolver().readPath('aaaaa'), 'aaaaa')

def my_processor(path):
return path.upper()

global PROCESSOR
PROCESSOR = my_processor
QgsPathResolver.setPathPreprocessor(PROCESSOR)
self.assertEqual(QgsPathResolver().readPath('aaaaa'), 'AAAAA')

def testLoadLayerWithPreprocessor(self):
"""
Test that custom path preprocessor is used when loading layers
"""
lines_shp_path = os.path.join(TEST_DATA_DIR, 'moooooo.shp')

lines_layer = QgsVectorLayer(lines_shp_path, 'Lines', 'ogr')
self.assertFalse(lines_layer.isValid())
p = QgsProject()
p.addMapLayer(lines_layer)
# save project to a temporary file
temp_path = tempfile.mkdtemp()
temp_project_path = os.path.join(temp_path, 'temp.qgs')
self.assertTrue(p.write(temp_project_path))

p2 = QgsProject()
self.assertTrue(p2.read(temp_project_path))
l = p2.mapLayersByName('Lines')[0]
self.assertEqual(l.name(), 'Lines')
self.assertFalse(l.isValid())

# custom processor to fix path
def my_processor(path):
return path.replace('moooooo', 'lines')

global PROCESSOR
PROCESSOR = my_processor
QgsPathResolver.setPathPreprocessor(PROCESSOR)
p3 = QgsProject()
self.assertTrue(p3.read(temp_project_path))
l = p3.mapLayersByName('Lines')[0]
self.assertEqual(l.name(), 'Lines')
# layer should have correct path now
self.assertTrue(l.isValid())


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

0 comments on commit 94412d1

Please sign in to comment.