Skip to content
Permalink
Browse files
Add QgsPathResolver::setPathWriter and QgsPathResolver::removePathWriter
  • Loading branch information
manisandro authored and nyalldawson committed Jun 18, 2021
1 parent e205ce8 commit c09be939f11f1bc114cc7830cd8371541d806b9e
Showing with 242 additions and 1 deletion.
  1. +73 −0 python/core/auto_generated/qgspathresolver.sip.in
  2. +24 −1 src/core/qgspathresolver.cpp
  3. +108 −0 src/core/qgspathresolver.h
  4. +37 −0 tests/src/python/test_qgspathresolver.py
@@ -137,6 +137,79 @@ The ``id`` must correspond to a pre-processor previously added via a call to :py
}
%End




static QString setPathWriter( SIP_PYCALLABLE / AllowNone / );
%Docstring
Sets a path writer function, which allows for manipulation of paths and data sources prior
to writing them to the project file.

The ``writer`` 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 writer function is called before any bad layer handler.

If multiple writers are set, they will be called in sequence based on the order in which
they were originally set.

Example - replace path with a variable:

.. code-block:: python

def my_processor(path):
return path.replace('c:/Users/ClintBarton/Documents/Projects', '@projectdir@')

QgsPathResolver.setPathWriter(my_processor)

.. versionadded:: 3.22
%End
%MethodCode
PyObject *s = 0;
Py_BEGIN_ALLOW_THREADS
Py_XINCREF( a0 );
QString id = QgsPathResolver::setPathWriter( [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;
} );

s = sipConvertFromNewType( new QString( id ), sipType_QString, 0 );
Py_END_ALLOW_THREADS
return s;
%End


static void removePathWriter( const QString &id );
%Docstring
Removes the custom writer function with matching ``id``.

The ``id`` must correspond to a writer previously added via a call to :py:func:`~QgsPathResolver.setPathWriter`.
An KeyError will be raised if no processor with the specified ``id`` exists.

.. seealso:: :py:func:`setPathWriter`

.. versionadded:: 3.22
%End
%MethodCode
if ( !QgsPathResolver::removePathWriter( *a0 ) )
{
PyErr_SetString( PyExc_KeyError, QStringLiteral( "No writer with id %1 exists." ).arg( *a0 ).toUtf8().constData() );
sipIsErr = 1;
}
%End

};

/************************************************************************
@@ -25,6 +25,7 @@

typedef std::vector< std::pair< QString, std::function< QString( const QString & ) > > > CustomResolvers;
Q_GLOBAL_STATIC( CustomResolvers, sCustomResolvers )
Q_GLOBAL_STATIC( CustomResolvers, sCustomWriters )

QgsPathResolver::QgsPathResolver( const QString &baseFileName )
: mBaseFileName( baseFileName )
@@ -189,8 +190,26 @@ bool QgsPathResolver::removePathPreprocessor( const QString &id )
return prevCount != sCustomResolvers()->size();
}

QString QgsPathResolver::writePath( const QString &src ) const
QString QgsPathResolver::setPathWriter( const std::function<QString( const QString & )> &writer )
{
QString id = QUuid::createUuid().toString();
sCustomWriters()->emplace_back( std::make_pair( id, writer ) );
return id;
}

bool QgsPathResolver::removePathWriter( const QString &id )
{
const size_t prevCount = sCustomWriters->size();
sCustomWriters()->erase( std::remove_if( sCustomWriters->begin(), sCustomWriters->end(), [id]( std::pair< QString, std::function< QString( const QString & ) > > &a )
{
return a.first == id;
} ), sCustomWriters->end() );
return prevCount != sCustomWriters->size();
}

QString QgsPathResolver::writePath( const QString &s ) const
{
QString src = s;
if ( src.isEmpty() )
{
return src;
@@ -200,6 +219,10 @@ QString QgsPathResolver::writePath( const QString &src ) const
if ( !localizedPath.isEmpty() )
return QStringLiteral( "localized:" ) + localizedPath;

const CustomResolvers customWriters = *sCustomWriters();
for ( const auto &writer : customWriters )
src = writer.second( src );

if ( src.startsWith( QgsApplication::pkgDataPath() + QStringLiteral( "/resources" ) ) )
{
// replace inbuilt data folder path with "inbuilt:" prefix
@@ -172,6 +172,114 @@ class CORE_EXPORT QgsPathResolver
% End
#endif



/**
* Sets a path writer function, which allows for manipulation of paths and data sources prior
* to writing them to the project file.
*
* The \a writer 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 writer function is called before any bad layer handler.
*
* If multiple writers 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 writer, which can later be
* used to remove the writer (via a call to removePathWriter()).
*
* \see removePathWriter()
* \since QGIS 3.22
*/
#ifndef SIP_RUN
static QString setPathWriter( const std::function< QString( const QString &filename )> &writer );
#else

/**
* Sets a path writer function, which allows for manipulation of paths and data sources prior
* to writing them to the project file.
*
* The \a writer 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 writer function is called before any bad layer handler.
*
* If multiple writers are set, they will be called in sequence based on the order in which
* they were originally set.
*
* Example - replace path with a variable:
*
* \code{.py}
* def my_processor(path):
* return path.replace('c:/Users/ClintBarton/Documents/Projects', '@projectdir@')
*
* QgsPathResolver.setPathWriter(my_processor)
* \endcode
*
* \since QGIS 3.22
*/
static QString setPathWriter( SIP_PYCALLABLE / AllowNone / );
% MethodCode
PyObject *s = 0;
Py_BEGIN_ALLOW_THREADS
Py_XINCREF( a0 );
QString id = QgsPathResolver::setPathWriter( [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;
} );

s = sipConvertFromNewType( new QString( id ), sipType_QString, 0 );
Py_END_ALLOW_THREADS
return s;
% End
#endif

/**
* Removes the custom writer function with matching \a id.
*
* The \a id must correspond to a writer previously added via a call to setPathWriter().
*
* Returns TRUE if writer existed and was removed.
*
* \see setPathWriter()
* \since QGIS 3.22
*/
#ifndef SIP_RUN
static bool removePathWriter( const QString &id );
#else

/**
* Removes the custom writer function with matching \a id.
*
* The \a id must correspond to a writer previously added via a call to setPathWriter().
* An KeyError will be raised if no processor with the specified \a id exists.
*
* \see setPathWriter()
* \since QGIS 3.22
*/
static void removePathWriter( const QString &id );
% MethodCode
if ( !QgsPathResolver::removePathWriter( *a0 ) )
{
PyErr_SetString( PyExc_KeyError, QStringLiteral( "No writer with id %1 exists." ).arg( *a0 ).toUtf8().constData() );
sipIsErr = 1;
}
% End
#endif

private:
//! path to a file that is the base for relative path resolution
QString mBaseFileName;
@@ -157,6 +157,43 @@ def testRelativeProject(self):
self.assertEqual(resolver.readPath('testlayer.shp').replace("\\", "/"), os.path.join(TEST_DATA_DIR, 'qgis_server', 'testlayer.shp').replace("\\", "/"))
os.chdir(curdir)

def __test__path_writer(self, path):
if path.startswith(TEST_DATA_DIR):
return os.path.join("@TEST_DATA_DIR@", os.path.basename(path))
return path

def __test_path_reader(self, path):
if path.startswith("@TEST_DATA_DIR@"):
return os.path.join(TEST_DATA_DIR, os.path.basename(path))
return path

def testPathWriter(self):
readerId = QgsPathResolver.setPathPreprocessor(self.__test_path_reader)
writerId = QgsPathResolver.setPathWriter(self.__test__path_writer)

lines_shp_path = os.path.join(TEST_DATA_DIR, 'lines.shp')

lines_layer = QgsVectorLayer(lines_shp_path, 'Lines', 'ogr')
self.assertTrue(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))

with open(temp_project_path) as f:
self.assertTrue("@TEST_DATA_DIR@" in f.read())

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

QgsPathResolver.removePathPreprocessor(readerId)
QgsPathResolver.removePathWriter(writerId)


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

0 comments on commit c09be93

Please sign in to comment.