[needs-docs] Revise QgsPathResolver::setPathPreprocessor API

Allow path preprocessors to be chained and don't force replace
any existing ones.

Processors can be removed via a call to QgsPathResolver::removePathPreprocessor,
using the unique ID returned by the original call to setPathPreprocessor
This commit is contained in:
Nyall Dawson 2019-08-22 09:07:08 +10:00
parent 3209cf2282
commit 24fb1196c5
4 changed files with 151 additions and 13 deletions

View File

@ -39,7 +39,7 @@ Turn filename read from the project file to an absolute path
%End
static void setPathPreprocessor( SIP_PYCALLABLE / AllowNone / );
static QString 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.
@ -85,9 +85,10 @@ Example - replace stored database credentials with new ones:
.. versionadded:: 3.10
%End
%MethodCode
PyObject *s = 0;
Py_BEGIN_ALLOW_THREADS
Py_XINCREF( a0 );
QgsPathResolver::setPathPreprocessor( [a0]( const QString &arg )->QString
QString id = QgsPathResolver::setPathPreprocessor( [a0]( const QString &arg )->QString
{
QString res;
SIP_BLOCK_THREADS
@ -104,7 +105,29 @@ Example - replace stored database credentials with new ones:
return res;
} );
s = sipConvertFromNewType( new QString( id ), sipType_QString, 0 );
Py_END_ALLOW_THREADS
return s;
%End
static void removePathPreprocessor( const QString &id );
%Docstring
Removes the custom pre-processor function with matching ``id``.
The ``id`` must correspond to a pre-processor previously added via a call to setPathPreprocessor().
An KeyError will be raised if no processor with the specified ``id`` exists.
.. seealso:: :py:func:`setPathPreprocessor`
.. versionadded:: 3.10
%End
%MethodCode
if ( !QgsPathResolver::removePathPreprocessor( *a0 ) )
{
PyErr_SetString( PyExc_KeyError, QStringLiteral( "No processor with id %1 exists." ).arg( *a0 ).toUtf8().constData() );
sipIsErr = 1;
}
%End
};

View File

@ -19,8 +19,9 @@
#include <QFileInfo>
#include <QUrl>
#include <QUuid>
std::function< QString( const QString & ) > QgsPathResolver::sCustomResolver = []( const QString &a )->QString { return a; };
std::vector< std::pair< QString, std::function< QString( const QString & ) > > > QgsPathResolver::sCustomResolvers;
QgsPathResolver::QgsPathResolver( const QString &baseFileName )
: mBaseFileName( baseFileName )
@ -30,7 +31,11 @@ QgsPathResolver::QgsPathResolver( const QString &baseFileName )
QString QgsPathResolver::readPath( const QString &f ) const
{
QString filename = sCustomResolver( f );
QString filename = f;
for ( const auto &resolver : sCustomResolvers )
filename = resolver.second( filename );
if ( filename.isEmpty() )
return QString();
@ -144,9 +149,21 @@ QString QgsPathResolver::readPath( const QString &f ) const
return vsiPrefix + projElems.join( QStringLiteral( "/" ) );
}
void QgsPathResolver::setPathPreprocessor( const std::function<QString( const QString & )> &processor )
QString QgsPathResolver::setPathPreprocessor( const std::function<QString( const QString & )> &processor )
{
sCustomResolver = processor;
QString id = QUuid::createUuid().toString();
sCustomResolvers.emplace_back( std::make_pair( id, processor ) );
return id;
}
bool QgsPathResolver::removePathPreprocessor( const QString &id )
{
const size_t prevCount = sCustomResolvers.size();
sCustomResolvers.erase( std::remove_if( sCustomResolvers.begin(), sCustomResolvers.end(), [id]( std::pair< QString, std::function< QString( const QString & ) > > &a )
{
return a.first == id;
} ), sCustomResolvers.end() );
return prevCount != sCustomResolvers.size();
}
QString QgsPathResolver::writePath( const QString &src ) const

View File

@ -20,6 +20,7 @@
#include <QString>
#include <functional>
#include <vector>
/**
* \ingroup core
@ -54,11 +55,16 @@ class CORE_EXPORT QgsPathResolver
*
* The path pre-processor function is called before any bad layer handler.
*
* \note Setting a new \a processor replaces any existing processor.
* If multiple preprocessors are set, they will be called in sequence based on the order in which
* they were originally set.
*
* \returns An auto-generated string uniquely identifying the preprocessor, which can later be
* used to remove the processor (via a call to removePathPreprocessor()).
*
* \see removePathPreprocessor()
* \since QGIS 3.10
*/
static void setPathPreprocessor( const std::function< QString( const QString &filename )> &processor );
static QString setPathPreprocessor( const std::function< QString( const QString &filename )> &processor );
#else
/**
@ -100,11 +106,12 @@ class CORE_EXPORT QgsPathResolver
*
* \since QGIS 3.10
*/
static void setPathPreprocessor( SIP_PYCALLABLE / AllowNone / );
static QString setPathPreprocessor( SIP_PYCALLABLE / AllowNone / );
% MethodCode
PyObject *s = 0;
Py_BEGIN_ALLOW_THREADS
Py_XINCREF( a0 );
QgsPathResolver::setPathPreprocessor( [a0]( const QString &arg )->QString
QString id = QgsPathResolver::setPathPreprocessor( [a0]( const QString &arg )->QString
{
QString res;
SIP_BLOCK_THREADS
@ -121,7 +128,43 @@ class CORE_EXPORT QgsPathResolver
return res;
} );
s = sipConvertFromNewType( new QString( id ), sipType_QString, 0 );
Py_END_ALLOW_THREADS
return s;
% End
#endif
#ifndef SIP_RUN
/**
* Removes the custom pre-processor function with matching \a id.
*
* The \a id must correspond to a pre-processor previously added via a call to setPathPreprocessor().
*
* Returns TRUE if processor existed and was removed.
*
* \see setPathPreprocessor()
* \since QGIS 3.10
*/
static bool removePathPreprocessor( const QString &id );
#else
/**
* Removes the custom pre-processor function with matching \a id.
*
* The \a id must correspond to a pre-processor previously added via a call to setPathPreprocessor().
* An KeyError will be raised if no processor with the specified \a id exists.
*
* \see setPathPreprocessor()
* \since QGIS 3.10
*/
static void removePathPreprocessor( const QString &id );
% MethodCode
if ( !QgsPathResolver::removePathPreprocessor( *a0 ) )
{
PyErr_SetString( PyExc_KeyError, QStringLiteral( "No processor with id %1 exists." ).arg( *a0 ).toUtf8().constData() );
sipIsErr = 1;
}
% End
#endif
@ -129,7 +172,7 @@ class CORE_EXPORT QgsPathResolver
//! path to a file that is the base for relative path resolution
QString mBaseFileName;
static std::function< QString( const QString & ) > sCustomResolver;
static std::vector< std::pair< QString, std::function< QString( const QString & ) > > > sCustomResolvers;
};
#endif // QGSPATHRESOLVER_H

View File

@ -28,20 +28,75 @@ class TestQgsPathResolver(unittest.TestCase):
def testCustomPreprocessor(self):
self.assertEqual(QgsPathResolver().readPath('aaaaa'), 'aaaaa')
with self.assertRaises(KeyError):
QgsPathResolver().removePathPreprocessor('bad')
def run_test():
def my_processor(path):
return path.upper()
QgsPathResolver.setPathPreprocessor(my_processor)
id = QgsPathResolver.setPathPreprocessor(my_processor)
self.assertTrue(id)
self.assertEqual(QgsPathResolver().readPath('aaaaa'), 'AAAAA')
return id
run_test()
id = run_test()
gc.collect()
# my_processor should be out of scope and cleaned up, unless things are working
# correctly and ownership was transferred
self.assertEqual(QgsPathResolver().readPath('aaaaa'), 'AAAAA')
QgsPathResolver().removePathPreprocessor(id)
self.assertEqual(QgsPathResolver().readPath('aaaaa'), 'aaaaa')
# expect key error
with self.assertRaises(KeyError):
QgsPathResolver().removePathPreprocessor(id)
def testChainedPreprocessors(self):
"""
Test that chaining preprocessors works correctly
"""
self.assertEqual(QgsPathResolver().readPath('aaaaa'), 'aaaaa')
def run_test():
def my_processor(path):
return 'x' + path + 'x'
def my_processor2(path):
return 'y' + path + 'y'
id = QgsPathResolver.setPathPreprocessor(my_processor)
self.assertTrue(id)
self.assertEqual(QgsPathResolver().readPath('aaaaa'), 'xaaaaax')
id2 = QgsPathResolver.setPathPreprocessor(my_processor2)
self.assertTrue(id2)
self.assertEqual(QgsPathResolver().readPath('aaaaa'), 'yxaaaaaxy')
return id, id2
id, id2 = run_test()
gc.collect()
# my_processor should be out of scope and cleaned up, unless things are working
# correctly and ownership was transferred
self.assertEqual(QgsPathResolver().readPath('aaaaa'), 'yxaaaaaxy')
QgsPathResolver().removePathPreprocessor(id)
self.assertEqual(QgsPathResolver().readPath('aaaaa'), 'yaaaaay')
# expect key error
with self.assertRaises(KeyError):
QgsPathResolver().removePathPreprocessor(id)
QgsPathResolver().removePathPreprocessor(id2)
self.assertEqual(QgsPathResolver().readPath('aaaaa'), 'aaaaa')
with self.assertRaises(KeyError):
QgsPathResolver().removePathPreprocessor(id2)
def testLoadLayerWithPreprocessor(self):
"""
Test that custom path preprocessor is used when loading layers