Skip to content

Commit

Permalink
Add QgsPathResolver::setPathWriter and QgsPathResolver::removePathWriter
Browse files Browse the repository at this point in the history
  • Loading branch information
manisandro authored and nyalldawson committed Jun 18, 2021
1 parent e205ce8 commit c09be93
Show file tree
Hide file tree
Showing 4 changed files with 242 additions and 1 deletion.
73 changes: 73 additions & 0 deletions python/core/auto_generated/qgspathresolver.sip.in
Expand Up @@ -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

};

/************************************************************************
Expand Down
25 changes: 24 additions & 1 deletion src/core/qgspathresolver.cpp
Expand Up @@ -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 )
Expand Down Expand Up @@ -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;
Expand All @@ -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
Expand Down
108 changes: 108 additions & 0 deletions src/core/qgspathresolver.h
Expand Up @@ -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;
Expand Down
37 changes: 37 additions & 0 deletions tests/src/python/test_qgspathresolver.py
Expand Up @@ -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.