Skip to content

Commit

Permalink
QML API 1.3: Add support for qrc:/ import paths. Fixes #2
Browse files Browse the repository at this point in the history
  • Loading branch information
thp committed Feb 16, 2014
1 parent ea0bc34 commit 5c8d9cf
Show file tree
Hide file tree
Showing 13 changed files with 166 additions and 8 deletions.
34 changes: 27 additions & 7 deletions docs/index.rst
Expand Up @@ -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.

Expand All @@ -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
----------------------

Expand All @@ -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
```````
Expand Down Expand Up @@ -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.
Expand All @@ -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`
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
========

Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
--------------------------
Expand Down
6 changes: 6 additions & 0 deletions 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)
6 changes: 5 additions & 1 deletion 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
Expand All @@ -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);
});
});
}
}
Expand Down
1 change: 1 addition & 0 deletions examples/qrc/data/qrc_example.qrc
Expand Up @@ -3,5 +3,6 @@
<qresource>
<file>qrc_example.qml</file>
<file>qrc_example.py</file>
<file>below/qrc_example_below.py</file>
</qresource>
</RCC>
1 change: 1 addition & 0 deletions src/pyotherside_plugin.cpp
Expand Up @@ -60,4 +60,5 @@ PyOtherSideExtensionPlugin::registerTypes(const char *uri)
qmlRegisterType<QPython10>(uri, 1, 0, PYOTHERSIDE_QPYTHON_NAME);
// There is no PyOtherSide 1.1 import, as it's the same as 1.0
qmlRegisterType<QPython12>(uri, 1, 2, PYOTHERSIDE_QPYTHON_NAME);
qmlRegisterType<QPython13>(uri, 1, 3, PYOTHERSIDE_QPYTHON_NAME);
}
9 changes: 9 additions & 0 deletions src/qpython.cpp
Expand Up @@ -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");
Expand Down
9 changes: 9 additions & 0 deletions src/qpython.h
Expand Up @@ -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 */
46 changes: 46 additions & 0 deletions src/qpython_priv.cpp
Expand Up @@ -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();
}
2 changes: 2 additions & 0 deletions src/qpython_priv.h
Expand Up @@ -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();
Expand Down
47 changes: 47 additions & 0 deletions src/qrc_importer.py
@@ -0,0 +1,47 @@
#
# PyOtherSide: Asynchronous Python 3 Bindings for Qt 5
# Copyright (c) 2014, Thomas Perl <m@thp.io>
#
# 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 "<module '{}' from '{}'>".format(m.__name__, m.__file__)

sys.meta_path.append(PyOtherSideQtRCImporter())
6 changes: 6 additions & 0 deletions src/qrc_importer.qrc
@@ -0,0 +1,6 @@
<!DOCTYPE RCC>
<RCC version="1.0">
<qresource prefix="/io/thp/pyotherside/">
<file>qrc_importer.py</file>
</qresource>
</RCC>
3 changes: 3 additions & 0 deletions src/src.pro
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions tests/tests.cpp
Expand Up @@ -153,4 +153,8 @@ TestPyOtherSide::testEvaluate()
// PyOtherSide API 1.2
QPython12 py12;
testEvaluateWith(&py12);

// PyOtherSide API 1.3
QPython13 py13;
testEvaluateWith(&py13);
}

0 comments on commit 5c8d9cf

Please sign in to comment.