diff --git a/docs/index.rst b/docs/index.rst
index 91ba0b2..e9c632f 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -28,7 +28,7 @@ This section describes the QML API exposed by the *PyOtherSide* QML Plugin.
Import Versions
---------------
-The current QML API version of PyOtherSide is 1.2. When new features are
+The current QML API version of PyOtherSide is 1.3. When new features are
introduced, or behavior is changed, the API version will be bumped and
documented here.
@@ -48,6 +48,13 @@ io.thp.pyotherside 1.2
:func:`importModule` or :func:`call`, the signal :func:`error` is emitted
with the exception information (filename, line, message) as ``traceback``.
+io.thp.pyotherside 1.3
+``````````````````````
+
+* :func:`addImportPath` now also accepts ``qrc:/`` URLs. This is useful if
+ your Python files are embedded as Qt Resources, relative to your QML files
+ (use :func:`Qt.resolvedUrl` from the QML file).
+
QML ``Python`` Element
----------------------
@@ -60,7 +67,7 @@ To use the ``Python`` element in a QML file, you have to import the plugin using
.. code-block:: javascript
- import io.thp.pyotherside 1.2
+ import io.thp.pyotherside 1.3
Signals
```````
@@ -89,13 +96,19 @@ path and then importing the module asynchronously:
.. function:: addImportPath(string path)
- Add a local filesystem path to Python's ``sys.path``.
+ Add a path to Python's ``sys.path``.
.. versionchanged:: 1.1.0
:func:`addImportPath` will automatically strip a leading
``file://`` from the path, so you can use :func:`Qt.resolvedUrl()`
without having to manually strip the leading ``file://`` in QML.
+.. versionchanged:: 1.3.0
+ Starting with QML API version 1.3 (``import io.thp.pyotherside 1.3``),
+ :func:`addImportPath` now also accepts ``qrc:/`` URLs. The first time
+ a ``qrc:/`` path is added, a new import handler will be installed,
+ which will enable Python to transparently import modules from it.
+
.. function:: importModule(string name, function callback(success) {})
Import a Python module.
@@ -104,7 +117,7 @@ path and then importing the module asynchronously:
Previously, this function didn't work correctly for importing
modules with dots in their name. Starting with the API version 1.2
(``import io.thp.pyotherside 1.2``), this behavior is now fixed,
- and ``importModule('x.y.z, ...)`` behaves like ``import x.y.z``.
+ and ``importModule('x.y.z', ...)`` behaves like ``import x.y.z``.
.. versionchanged:: 1.2.0
If a JavaScript exception occurs in the callback, the :func:`error`
@@ -152,7 +165,7 @@ plugin and Python interpreter.
.. note::
This is not necessarily the same as the QML API version currently in use.
The QML API version is decided by the QML import statement, so even if
- :func:`pluginVersion`` returns 1.2.0, if the plugin has been imported as
+ :func:`pluginVersion` returns 1.2.0, if the plugin has been imported as
``import io.thp.pyotherside 1.0``, the API version used would be 1.0.
.. versionadded:: 1.1.0
@@ -406,6 +419,12 @@ walking the whole resource tree, printing out directory names and file sizes:
walk('/')
+Importing Python modules from Qt Resources also works starting with QML API 1.3
+using :func:`Qt.resolvedUrl` from within a QML file in Qt Resources. As an
+alternative, ``addImportPath('qrc:/')`` will add the root directory of the Qt
+Resources to Python's module search path.
+
+
Cookbook
========
@@ -597,7 +616,7 @@ Using this function from QML is straightforward:
.. code-block:: javascript
import QtQuick 2.0
- import io.thp.pyotherside 1.2
+ import io.thp.pyotherside 1.3
Rectangle {
color: 'black'
@@ -693,7 +712,7 @@ This module can now be imported in QML and used as ``source`` in the QML
.. code-block:: javascript
import QtQuick 2.0
- import io.thp.pyotherside 1.2
+ import io.thp.pyotherside 1.3
Image {
id: image
@@ -784,6 +803,7 @@ Version 1.3.0 (UNRELEASED)
--------------------------
* Access to the `Qt Resource System`_ from Python (see `Qt Resource Access`_).
+* QML API 1.3: Import from Qt Resources (:func:`addImportPath` with ``qrc:/``).
Version 1.2.0 (2014-02-16)
--------------------------
diff --git a/examples/qrc/data/below/qrc_example_below.py b/examples/qrc/data/below/qrc_example_below.py
new file mode 100644
index 0000000..7aeb1be
--- /dev/null
+++ b/examples/qrc/data/below/qrc_example_below.py
@@ -0,0 +1,6 @@
+import sys
+import pyotherside
+
+print('Hello from below!')
+print('sys.path =', sys.path)
+print('pyotherside =', pyotherside)
diff --git a/examples/qrc/data/qrc_example.qml b/examples/qrc/data/qrc_example.qml
index 85894e1..3b60884 100644
--- a/examples/qrc/data/qrc_example.qml
+++ b/examples/qrc/data/qrc_example.qml
@@ -1,5 +1,5 @@
import QtQuick 2.0
-import io.thp.pyotherside 1.2
+import io.thp.pyotherside 1.3
Rectangle {
width: 100
@@ -10,6 +10,10 @@ Rectangle {
addImportPath(Qt.resolvedUrl('.'));
importModule('qrc_example', function (success) {
console.log('module imported: ' + success);
+ addImportPath(Qt.resolvedUrl('below'));
+ importModule('qrc_example_below', function (success) {
+ console.log('also imported: ' + success);
+ });
});
}
}
diff --git a/examples/qrc/data/qrc_example.qrc b/examples/qrc/data/qrc_example.qrc
index 666f34e..148866d 100644
--- a/examples/qrc/data/qrc_example.qrc
+++ b/examples/qrc/data/qrc_example.qrc
@@ -3,5 +3,6 @@
qrc_example.qml
qrc_example.py
+ below/qrc_example_below.py
diff --git a/src/pyotherside_plugin.cpp b/src/pyotherside_plugin.cpp
index c03d52c..33afc4e 100644
--- a/src/pyotherside_plugin.cpp
+++ b/src/pyotherside_plugin.cpp
@@ -60,4 +60,5 @@ PyOtherSideExtensionPlugin::registerTypes(const char *uri)
qmlRegisterType(uri, 1, 0, PYOTHERSIDE_QPYTHON_NAME);
// There is no PyOtherSide 1.1 import, as it's the same as 1.0
qmlRegisterType(uri, 1, 2, PYOTHERSIDE_QPYTHON_NAME);
+ qmlRegisterType(uri, 1, 3, PYOTHERSIDE_QPYTHON_NAME);
}
diff --git a/src/qpython.cpp b/src/qpython.cpp
index b6ecc0b..5bd7566 100644
--- a/src/qpython.cpp
+++ b/src/qpython.cpp
@@ -83,6 +83,15 @@ QPython::addImportPath(QString path)
path = path.mid(7);
}
+ if (SINCE_API_VERSION(1, 3) && path.startsWith("qrc:")) {
+ const char *module = "pyotherside.qrc_importer";
+ QString filename = "/io/thp/pyotherside/qrc_importer.py";
+ QString errorMessage = priv->importFromQRC(module, filename);
+ if (!errorMessage.isNull()) {
+ emit error(errorMessage);
+ }
+ }
+
QByteArray utf8bytes = path.toUtf8();
PyObject *sys_path = PySys_GetObject((char*)"path");
diff --git a/src/qpython.h b/src/qpython.h
index 96821d7..79c6d9d 100644
--- a/src/qpython.h
+++ b/src/qpython.h
@@ -317,4 +317,13 @@ public:
}
};
+class QPython13 : public QPython {
+Q_OBJECT
+public:
+ QPython13(QObject *parent=0)
+ : QPython(parent, 1, 3)
+ {
+ }
+};
+
#endif /* PYOTHERSIDE_QPYTHON_H */
diff --git a/src/qpython_priv.cpp b/src/qpython_priv.cpp
index 7342925..41bc659 100644
--- a/src/qpython_priv.cpp
+++ b/src/qpython_priv.cpp
@@ -379,3 +379,49 @@ QPythonPriv::instance()
{
return priv;
}
+
+QString
+QPythonPriv::importFromQRC(const char *module, const QString &filename)
+{
+ PyObject *sys_modules = PySys_GetObject((char *)"modules");
+ if (!PyMapping_Check(sys_modules)) {
+ return QString("sys.modules is not a mapping object");
+ }
+
+ PyObject *qrc_importer = PyMapping_GetItemString(sys_modules,
+ (char *)module);
+
+ if (qrc_importer == NULL) {
+ PyErr_Clear();
+
+ QFile qrc_importer_code(":" + filename);
+ if (!qrc_importer_code.open(QIODevice::ReadOnly)) {
+ return QString("Cannot load qrc importer source");
+ }
+
+ QByteArray ba = qrc_importer_code.readAll();
+ QByteArray fn = QString("qrc:/" + filename).toUtf8();
+
+ PyObject *co = Py_CompileString(ba.constData(), fn.constData(),
+ Py_file_input);
+ if (co == NULL) {
+ QString result = QString("Cannot compile qrc importer: %1")
+ .arg(formatExc());
+ PyErr_Clear();
+ return result;
+ }
+
+ qrc_importer = PyImport_ExecCodeModule((char *)module, co);
+ if (qrc_importer == NULL) {
+ QString result = QString("Cannot exec qrc importer: %1")
+ .arg(formatExc());
+ PyErr_Clear();
+ return result;
+ }
+ Py_XDECREF(co);
+ }
+
+ Py_XDECREF(qrc_importer);
+
+ return QString();
+}
diff --git a/src/qpython_priv.h b/src/qpython_priv.h
index 5d9a6c6..190409b 100644
--- a/src/qpython_priv.h
+++ b/src/qpython_priv.h
@@ -38,6 +38,8 @@ class QPythonPriv : public QObject {
void enter();
void leave();
+ QString importFromQRC(const char *module, const QString &filename);
+
void receiveObject(PyObject *o);
static void closing();
static QPythonPriv *instance();
diff --git a/src/qrc_importer.py b/src/qrc_importer.py
new file mode 100644
index 0000000..9d8281e
--- /dev/null
+++ b/src/qrc_importer.py
@@ -0,0 +1,47 @@
+#
+# PyOtherSide: Asynchronous Python 3 Bindings for Qt 5
+# Copyright (c) 2014, Thomas Perl
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
+# FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+# OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+#
+
+import sys
+import pyotherside
+
+from importlib import abc
+
+class PyOtherSideQtRCImporter(abc.MetaPathFinder, abc.SourceLoader):
+ def find_module(self, fullname, path):
+ if path is None or all(x.startswith('qrc:') for x in path):
+ if self.get_filename(fullname):
+ return self
+
+ def get_filename(self, fullname):
+ basename = fullname.replace('.', '/')
+
+ for import_path in sys.path:
+ if not import_path.startswith('qrc:'):
+ continue
+
+ for candidate in ('{}/{}.py', '{}/{}/__init__.py'):
+ filename = candidate.format(import_path, basename)
+ if pyotherside.qrc_is_file(filename[len('qrc:'):]):
+ return filename
+
+ def get_data(self, path):
+ return pyotherside.qrc_get_file_contents(path[len('qrc:'):])
+
+ def module_repr(self, m):
+ return "".format(m.__name__, m.__file__)
+
+sys.meta_path.append(PyOtherSideQtRCImporter())
diff --git a/src/qrc_importer.qrc b/src/qrc_importer.qrc
new file mode 100644
index 0000000..78a0da9
--- /dev/null
+++ b/src/qrc_importer.qrc
@@ -0,0 +1,6 @@
+
+
+
+ qrc_importer.py
+
+
diff --git a/src/src.pro b/src/src.pro
index 40de987..9f382a3 100644
--- a/src/src.pro
+++ b/src/src.pro
@@ -28,6 +28,9 @@ HEADERS += pyotherside_plugin.h
SOURCES += qpython_imageprovider.cpp
HEADERS += qpython_imageprovider.h
+# Importer from Qt Resources
+RESOURCES += qrc_importer.qrc
+
# Python QML Object
SOURCES += qpython.cpp
HEADERS += qpython.h
diff --git a/tests/tests.cpp b/tests/tests.cpp
index 62f505f..a56090d 100644
--- a/tests/tests.cpp
+++ b/tests/tests.cpp
@@ -153,4 +153,8 @@ TestPyOtherSide::testEvaluate()
// PyOtherSide API 1.2
QPython12 py12;
testEvaluateWith(&py12);
+
+ // PyOtherSide API 1.3
+ QPython13 py13;
+ testEvaluateWith(&py13);
}