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 @@ Q_OBJECT } }; +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); }