[Merge] lp:~3v1n0/ubuntu/vivid/pyotherside/bump-to-1.4.0-git20150111 into lp:ubuntu/pyotherside

Marco Trevisan (Treviño) mail at 3v1n0.net
Fri Feb 13 15:04:11 UTC 2015


Well, then I think we'd need someone to sponsor us... :)

Diff comments:

> === modified file 'debian/changelog'
> --- debian/changelog	2014-04-30 15:58:00 +0000
> +++ debian/changelog	2015-02-13 14:24:18 +0000
> @@ -1,3 +1,9 @@
> +pyotherside (1.4.0~git20150111-1ubuntu1) UNRELEASED; urgency=medium
> +
> +  * Import latest git version, includes various fixes and features
> +
> + -- Marco Trevisan (Treviño) <mail at 3v1n0.net>  Fri, 13 Feb 2015 15:08:46 +0100
> +
>  pyotherside (1.2.0-1) unstable; urgency=medium
>  
>    * Initial release. (Closes: #746294)
> 
> === modified file 'debian/control'
> --- debian/control	2014-04-30 15:58:00 +0000
> +++ debian/control	2015-02-13 14:24:18 +0000
> @@ -14,7 +14,7 @@
>                 xauth,
>                 xvfb
>  Standards-Version: 3.9.5
> -X-Python3-Version: >= 3.2
> +X-Python3-Version: >= 3.3

I've not tested that version, but the README states that it needs Python >= 3.3, so I think it's safer to assume that.

>  XS-Testsuite: autopkgtest
>  Vcs-Svn: svn://anonscm.debian.org/python-modules/packages/pyotherside/trunk/
>  Vcs-Browser: http://anonscm.debian.org/viewvc/python-modules/packages/pyotherside/trunk/
> 
> === modified file 'docs/conf.py'
> --- docs/conf.py	2014-04-30 15:58:00 +0000
> +++ docs/conf.py	2015-02-13 14:24:18 +0000
> @@ -48,9 +48,9 @@
>  # built documents.
>  #
>  # The short X.Y version.
> -version = '1.2'
> +version = '1.3'
>  # The full version, including alpha/beta/rc tags.
> -release = '1.2.0'
> +release = '1.3.0'
>  
>  # The language for content autogenerated by Sphinx. Refer to documentation
>  # for a list of supported languages.
> 
> === modified file 'docs/index.rst'
> --- docs/index.rst	2014-04-30 15:58:00 +0000
> +++ docs/index.rst	2015-02-13 14:24:18 +0000
> @@ -13,7 +13,7 @@
>  objects to Python objects and vice versa, with focus on asynchronous events
>  and continuation-passing style function calls.
>  
> -While PyOtherSide once worked with Qt 4.x, and still works with Python 2.7, its
> +While legacy versions of PyOtherSide worked with Qt 4.x and Python 2.x, its
>  focus now lies on Python 3.x and Qt 5. Python 3 has been out for several years,
>  and offers some nice language features and clean-ups, while Qt 5 supports most
>  mobile platforms well, and has an improved QML engine and a faster renderer (Qt
> @@ -28,7 +28,7 @@
>  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 @@
>    :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 @@
>  
>  .. code-block:: javascript
>  
> -    import io.thp.pyotherside 1.2
> +    import io.thp.pyotherside 1.3
>  
>  Signals
>  ```````
> @@ -89,13 +96,19 @@
>  
>  .. 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 @@
>      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`
> @@ -126,6 +139,23 @@
>      signal is emitted with ``traceback`` containing the exception info
>      (QML API version 1.2 and newer).
>  
> +.. function:: callMethod(obj, string method, args=[], function callback(result) {})
> +
> +    Call the Python method ``method`` on object ``obj`` with ``args``
> +    asynchronously.
> +    If ``args`` is omitted, ``method`` will be called without arguments.
> +    If ``callback`` is a callable, it will be called with the Python
> +    method result as single argument when the call has succeeded.
> +
> +    If a JavaScript exception occurs in the callback, the :func:`error`
> +    signal is emitted with ``traceback`` containing the exception info.
> +
> +Attributes on Python objects can be accessed using :func:`getattr`:
> +
> +.. function:: getattr(obj, string attr) -> var
> +
> +    Get the attribute ``attr`` of the Python object ``obj``.
> +
>  For some of these methods, there also exist synchronous variants, but it is
>  highly recommended to use the asynchronous variants instead to avoid blocking
>  the QML UI thread:
> @@ -142,6 +172,10 @@
>  
>      Call a Python function. Returns the return value of the Python function.
>  
> +.. function:: callMethod_sync(obj, string method, var args=[]) -> var
> +
> +    Call a Python method. Returns the return value of the Python method.
> +
>  The following functions allow access to the version of the running PyOtherSide
>  plugin and Python interpreter.
>  
> @@ -152,7 +186,7 @@
>  .. 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
> @@ -201,6 +235,42 @@
>  
>  .. versionadded:: 1.1.0
>  
> +.. function:: pyotherside.qrc_is_file(filename)
> +
> +    Check if ``filename`` is an existing file in the `Qt Resource System`_.
> +
> +    :returns: ``True`` if ``filename`` is a file, ``False`` otherwise.
> +
> +.. versionadded:: 1.3.0
> +
> +.. function:: pyotherside.qrc_is_dir(dirname)
> +
> +    Check if ``dirname`` is an existing directory in the `Qt Resource System`_.
> +
> +    :returns: ``True`` if ``dirname`` is a directory, ``False`` otherwise.
> +
> +.. versionadded:: 1.3.0
> +
> +.. function:: pyotherside.qrc_get_file_contents(filename)
> +
> +    Get the file contents of a file in the `Qt Resource System`_.
> +
> +    :raise ValueError: If ``filename`` does not denote a valid file.
> +    :returns: The file contents as Python ``bytearray`` object.
> +
> +.. versionadded:: 1.3.0
> +
> +.. function:: pyotherside.qrc_list_dir(dirname)
> +
> +    Get the entry list of a directory in the `Qt Resource System`_.
> +
> +    :raise ValueError: If ``dirname`` does not denote a valid directory.
> +    :returns: The directory entries as list of strings.
> +
> +.. versionadded:: 1.3.0
> +
> +.. _Qt Resource System: http://qt-project.org/doc/qt-5/resources.html
> +
>  .. _constants:
>  
>  Constants
> @@ -240,6 +310,14 @@
>  **pyotherside.format_data**
>      Encoded image file data (e.g. PNG/JPEG data).
>  
> +.. versionadded:: 1.3.0
> +
> +The following constants have been added in PyOtherSide 1.3:
> +
> +**pyotherside.version**
> +    Version of PyOtherSide as string.
> +
> +
>  
>  Data Type Mapping
>  =================
> @@ -260,10 +338,10 @@
>  +--------------------+------------+-----------------------------+
>  | str                | string     |                             |
>  +--------------------+------------+-----------------------------+
> -| list               | JS Array   |                             |
> +| list               | JS Array   | JS Arrays are always        |
> +|                    |            | converted to Python lists.  |
>  +--------------------+------------+-----------------------------+
> -| tuple              | JS Array   | JS Arrays are converted to  |
> -|                    |            | lists, not tuples           |
> +| tuple              | JS Array   |                             |
>  +--------------------+------------+-----------------------------+
>  | dict               | JS Object  | Keys must be strings        |
>  +--------------------+------------+-----------------------------+
> @@ -273,6 +351,10 @@
>  +--------------------+------------+-----------------------------+
>  | datetime.datetime  | JS Date    | since PyOtherSide 1.2.0     |
>  +--------------------+------------+-----------------------------+
> +| set                | JS Array   | since PyOtherSide 1.3.0     |
> ++--------------------+------------+-----------------------------+
> +| iterable           | JS Array   | since PyOtherSide 1.3.0     |
> ++--------------------+------------+-----------------------------+
>  
>  Trying to pass in other types than the ones listed here is undefined
>  behavior and will usually result in an error.
> @@ -340,6 +422,42 @@
>  the image provider has been set (e.g. by setting the ``source`` property
>  in the callback function passed to :func:`importModule`).
>  
> +.. _qt resource access:
> +
> +Qt Resource Access
> +==================
> +
> +.. versionadded:: 1.3.0
> +
> +If you are using PyOtherSide in combination with an application binary compiled
> +from C++ code with Qt Resources (see `Qt Resource System`_), you can inspect
> +and access the resources from Python. This example demonstrates the API by
> +walking the whole resource tree, printing out directory names and file sizes:
> +
> +.. code-block:: python
> +
> +    import pyotherside
> +    import os.path
> +
> +    def walk(root):
> +        for entry in pyotherside.qrc_list_dir(root):
> +            name = os.path.join(root, entry)
> +            if pyotherside.qrc_is_dir(name):
> +                print('Directory:', name)
> +                walk(name)
> +            else:
> +                data = pyotherside.qrc_get_file_contents(name)
> +                print('File:', name, 'has', len(data), 'bytes')
> +
> +    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
>  ========
>  
> @@ -382,6 +500,36 @@
>  
>  .. _Continuation-passing style: https://en.wikipedia.org/wiki/Continuation-passing_style
>  
> +To avoid what's called `callback hell`_ in JavaScript, you can pull out the
> +anonymous functions you give as callbacks, give them names and pass them to
> +the API functions via name, e.g. the above example would turn into a shallow
> +structure (of course, in this example, splitting everything out does not make
> +too much sense, as the functions are very simple to begin with, but it's here
> +to demonstrate how splitting a callback hell pyramid basically works):
> +
> +.. _callback hell: http://callbackhell.com/
> +
> +.. code-block:: javascript
> +
> +    Python {
> +        Component.onCompleted: {
> +            function changedCwd(result) {
> +                console.log('Working directory changed.');
> +            }
> +
> +            function gotCwd(result) {
> +                console.log('Working directory: ' + result);
> +                call('os.chdir', ['/'], changedCwd);
> +            }
> +
> +            function withOs() {
> +                call('os.getcwd', [], gotCwd);
> +            }
> +
> +            importModule('os', withOs);
> +        }
> +    }
> +
>  Evaluating Python expressions in QML
>  ````````````````````````````````````
>  
> @@ -531,7 +679,7 @@
>  .. code-block:: javascript
>  
>      import QtQuick 2.0
> -    import io.thp.pyotherside 1.2
> +    import io.thp.pyotherside 1.3
>  
>      Rectangle {
>          color: 'black'
> @@ -579,6 +727,8 @@
>  Rendering RGBA image data in Python
>  -----------------------------------
>  
> +.. versionadded:: 1.1.0
> +
>  .. image:: images/image_provider_example.png
>  
>  This example uses the `image provider`_ feature of PyOtherSide to
> @@ -625,7 +775,7 @@
>  .. code-block:: javascript
>  
>      import QtQuick 2.0
> -    import io.thp.pyotherside 1.2
> +    import io.thp.pyotherside 1.3
>  
>      Image {
>          id: image
> @@ -646,7 +796,6 @@
>          }
>      }
>  
> -
>  Building PyOtherSide
>  ====================
>  
> @@ -676,11 +825,7 @@
>  Alternatively, you can edit ``python.pri`` manually and specify the compiler
>  flags for compiling and linking against Python on your system.
>  
> -As of version 1.1.0, PyOtherSide still builds against Python 2.x (tested with
> -Python 2.7, use ``qmake PYTHON_CONFIG=python2.7-config``), but future point
> -releases of PyOtherSide might drop support for Python 2.x. However, only one
> -version of PyOtherSide can be installed/active at one time. It is highly
> -recommended that you do not use Python 2 support except on legacy platforms.
> +As of version 1.3.0, PyOtherSide does not build against Python 2.x anymore.
>  
>  Building for Blackberry 10
>  --------------------------
> @@ -709,10 +854,80 @@
>  After installing PyOtherSide in the locally-build Qt 5 (cross-compiled for
>  BB10), the QML plugins folder can be deployed with the .bar file.
>  
> +Building for Windows
> +--------------------
> +
> +On Windows (tested versions: Windows 7), you need to download:
> +
> +1. Qt 5 (VS 2010) from `qt-project.org downloads`_ (tested: 5.2.1)
> +2. `Visual C++ 2010 Express`_
> +3. Python 3 from `python.org Windows downloads`_ (tested: 3.3.4)
> +
> +We use VS 2010 instead of MinGW, because the MinGW version of Qt depends on
> +working OpenGL driver, whereas the non-OpenGL version uses Direct3D via ANGLE.
> +Also, Python is built with Visual C++ 2010 Express (see `Compiling Python on
> +Windows`_), so using the same toolchain when linking all three components (Qt,
> +Python and PyOtherSide) together makes sense.
> +
> +The necessary customizations for building PyOtherSide successfully on Windows
> +have been integrated recently, and are available since PyOtherSide 1.3.0.
> +
> +.. _qt-project.org downloads: http://qt-project.org/downloads
> +.. _Visual C++ 2010 Express: http://www.visualstudio.com/en-us/downloads/download-visual-studio-vs#DownloadFamilies_4
> +.. _python.org Windows downloads: http://python.org/downloads/windows/
> +.. _Compiling Python on Windows: http://docs.python.org/devguide/setup.html#windows-compiling
> +
> +Once these pre-requisites are installed, you need to make some customizations
> +to the build setup:
> +
> +1. In ``src/qmldir``: Change ``plugin pyothersideplugin`` to ``plugin
> +   pyothersideplugin1``. This is needed, because on Windows, the library
> +   version gets encoded into the library name.
> +
> +2. In ``python.pri``: Modify it so that the Python 3 ``libs/`` folder is
> +   added to the linker path, and link against ``-lpython33``. Also, modify
> +   it so that the Python 3 ``include/`` folder is added to the compiler flags.
> +
> +Example ``python.pri`` file for a standard Python 3.3 installation on Windows:
> +
> +.. code-block:: qmake
> +
> +    QMAKE_LIBS += -LC:\Python33\libs -lpython33
> +    QMAKE_CXXFLAGS += -IC:\Python33\include\
> +
> +With the updated ``qmldir`` and ``python.pri`` files in place, simply open
> +the ``pyotherside.pro`` project file in Qt Creator, and build the project.
> +Configure a **Release Build**, and *disable* **Shadow Builds**.
> +
> +To install PyOtherSide into your Qt installation, so that the QML import works
> +from other projects:
> +
> +1. Make sure the PyOtherSide project is opened in Qt Creator
> +2. In the left column, select **Projects**
> +3. Make sure the **Run** tab (Run Settings) of your project is selected
> +4. In **Deployment**, click **Add Deploy Step** and select **Make**
> +5. In the **Make arguments:** field, type ``install``
> +6. Hit **Run** to install PyOtherSide in your local Qt folder
> +7. Dismiss the "Custom Executable" dialog that pops up
> +
> +Known Problems:
> +
> +* **Qt Resource System** importing might not fully work on Windows
> +
>  
>  ChangeLog
>  =========
>  
> +Version 1.3.0 (2014-07-24)
> +--------------------------
> +
> +* Access to the `Qt Resource System`_ from Python (see `Qt Resource Access`_).
> +* QML API 1.3: Import from Qt Resources (:func:`addImportPath` with ``qrc:/``).
> +* Add ``pyotherside.version`` constant to access version from Python as string.
> +* Support for building on Windows, build instructions for Windows builds.
> +* New data type conversions: Python ``set`` and iterable types (e.g. generator
> +  expressions and generators) are converted to JS ``Array``.
> +
>  Version 1.2.0 (2014-02-16)
>  --------------------------
>  
> 
> === modified file 'examples/events_example.py'
> --- examples/events_example.py	2014-04-30 15:58:00 +0000
> +++ examples/events_example.py	2015-02-13 14:24:18 +0000
> @@ -5,6 +5,8 @@
>  import threading
>  import time
>  
> +print('Using PyOtherSide version', pyotherside.version)
> +
>  COLORS = ['red', 'green', 'blue']
>  
>  def thread_func():
> 
> === added directory 'examples/qrc'
> === added directory 'examples/qrc/data'
> === added directory 'examples/qrc/data/below'
> === added file 'examples/qrc/data/below/qrc_example_below.py'
> --- examples/qrc/data/below/qrc_example_below.py	1970-01-01 00:00:00 +0000
> +++ examples/qrc/data/below/qrc_example_below.py	2015-02-13 14:24:18 +0000
> @@ -0,0 +1,6 @@
> +import sys
> +import pyotherside
> +
> +print('Hello from below!')
> +print('sys.path =', sys.path)
> +print('pyotherside =', pyotherside)
> 
> === added file 'examples/qrc/data/qrc_example.py'
> --- examples/qrc/data/qrc_example.py	1970-01-01 00:00:00 +0000
> +++ examples/qrc/data/qrc_example.py	2015-02-13 14:24:18 +0000
> @@ -0,0 +1,43 @@
> +import pyotherside
> +import os.path
> +import sys
> +
> +print('Hello from module!')
> +print(sys.path)
> +print('file exists?', pyotherside.qrc_is_file('qrc_example.qml'))
> +print('file exists?', pyotherside.qrc_is_file('qrc_example.qml.nonexistent'))
> +print('dir exists?', pyotherside.qrc_is_dir('/'))
> +print('dir exists?', pyotherside.qrc_is_dir('/nonexistent'))
> +
> +print('='*30)
> +def walk(root):
> +    for entry in pyotherside.qrc_list_dir(root):
> +        name = os.path.join(root, entry)
> +        if pyotherside.qrc_is_dir(name):
> +            walk(name)
> +        else:
> +            print(name, '=', len(pyotherside.qrc_get_file_contents(name)), 'bytes')
> +walk('/')
> +print('='*30)
> +print(pyotherside.qrc_get_file_contents('qrc_example.py').decode('utf-8'))
> +print('='*30)
> +
> +try:
> +    print('dir exists with number', pyotherside.qrc_is_dir(123))
> +except Exception as e:
> +    print('got exception (as expected):', e)
> +
> +try:
> +    print('file exists with none', pyotherside.qrc_is_file(None))
> +except Exception as e:
> +    print('got exception (as expected):', e)
> +
> +try:
> +    print('dir entries with invalid', pyotherside.qrc_list_dir('/nonexistent'))
> +except Exception as e:
> +    print('got exception (as expected):', e)
> +
> +try:
> +    print('file contents with invalid', pyotherside.qrc_get_file_contents('/qrc_example.qml.nonexistent'))
> +except Exception as e:
> +    print('got exception (as expected):', e)
> 
> === added file 'examples/qrc/data/qrc_example.qml'
> --- examples/qrc/data/qrc_example.qml	1970-01-01 00:00:00 +0000
> +++ examples/qrc/data/qrc_example.qml	2015-02-13 14:24:18 +0000
> @@ -0,0 +1,20 @@
> +import QtQuick 2.0
> +import io.thp.pyotherside 1.3
> +
> +Rectangle {
> +    width: 100
> +    height: 100
> +
> +    Python {
> +        Component.onCompleted: {
> +            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);
> +                });
> +            });
> +        }
> +    }
> +}
> 
> === added file 'examples/qrc/data/qrc_example.qrc'
> --- examples/qrc/data/qrc_example.qrc	1970-01-01 00:00:00 +0000
> +++ examples/qrc/data/qrc_example.qrc	2015-02-13 14:24:18 +0000
> @@ -0,0 +1,8 @@
> +<!DOCTYPE RCC>
> +<RCC version="1.0">
> +  <qresource>
> +    <file>qrc_example.qml</file>
> +    <file>qrc_example.py</file>
> +    <file>below/qrc_example_below.py</file>
> +  </qresource>
> +</RCC>
> 
> === added file 'examples/qrc/qrc_example.cpp'
> --- examples/qrc/qrc_example.cpp	1970-01-01 00:00:00 +0000
> +++ examples/qrc/qrc_example.cpp	2015-02-13 14:24:18 +0000
> @@ -0,0 +1,12 @@
> +#include <QGuiApplication>
> +#include <QQuickView>
> +#include <QUrl>
> +
> +int main(int argc, char *argv[])
> +{
> +    QGuiApplication app(argc, argv);
> +    QQuickView view;
> +    view.setSource(QUrl("qrc:/qrc_example.qml"));
> +    view.show();
> +    return app.exec();
> +}
> 
> === added file 'examples/qrc/qrc_example.pro'
> --- examples/qrc/qrc_example.pro	1970-01-01 00:00:00 +0000
> +++ examples/qrc/qrc_example.pro	2015-02-13 14:24:18 +0000
> @@ -0,0 +1,10 @@
> +TARGET = qrc_example
> +
> +TEMPLATE = app
> +DEPENDPATH += .
> +INCLUDEPATH += .
> +
> +QT += qml quick
> +
> +SOURCES += qrc_example.cpp
> +RESOURCES += data/qrc_example.qrc
> 
> === modified file 'pyotherside.pri'
> --- pyotherside.pri	2014-04-30 15:58:00 +0000
> +++ pyotherside.pri	2015-02-13 14:24:18 +0000
> @@ -1,2 +1,2 @@
>  PROJECTNAME = pyotherside
> -VERSION = 1.2.0
> +VERSION = 1.3.0
> 
> === modified file 'pyotherside.pro'
> --- pyotherside.pro	2014-04-30 15:58:00 +0000
> +++ pyotherside.pro	2015-02-13 14:24:18 +0000
> @@ -5,14 +5,19 @@
>  
>  include(pyotherside.pri)
>  
> -tar.target = $${PROJECTNAME}-$${VERSION}.tar
> -tar.commands = git archive --format=tar --prefix=$${PROJECTNAME}-$${VERSION}/ --output=$@ $${VERSION}
> -
> -targz.target = $${PROJECTNAME}-$${VERSION}.tar.gz
> -targz.depends = tar
> -targz.commands = gzip $^
> -
> -sdist.target = sdist
> -sdist.depends = targz
> -
> -QMAKE_EXTRA_TARGETS += tar targz sdist
> +!win32 {
> +    # The make used in the Qt MSVC toolchain does not support $^, but
> +    # as we are not going to do source builds on Windows, just make
> +    # the source release (sdist) target depend on anything but win32.
> +    tar.target = $${PROJECTNAME}-$${VERSION}.tar
> +    tar.commands = git archive --format=tar --prefix=$${PROJECTNAME}-$${VERSION}/ --output=$@ $${VERSION}
> +
> +    targz.target = $${PROJECTNAME}-$${VERSION}.tar.gz
> +    targz.depends = tar
> +    targz.commands = gzip $^
> +
> +    sdist.target = sdist
> +    sdist.depends = targz
> +
> +    QMAKE_EXTRA_TARGETS += tar targz sdist
> +}
> 
> === modified file 'src/converter.h'
> --- src/converter.h	2014-04-30 15:58:00 +0000
> +++ src/converter.h	2015-02-13 14:24:18 +0000
> @@ -19,6 +19,8 @@
>  #ifndef PYOTHERSIDE_CONVERTER_H
>  #define PYOTHERSIDE_CONVERTER_H
>  
> +#include "pyobject_ref.h"
> +
>  struct ConverterDate {
>      ConverterDate(int y, int m, int d)
>          : y(y), m(m), d(d)
> @@ -73,7 +75,6 @@
>          ListIterator() {}
>          virtual ~ListIterator() {}
>  
> -        virtual int count() = 0;
>          virtual bool next(V*) = 0;
>  };
>  
> @@ -103,6 +104,7 @@
>              DATE,
>              TIME,
>              DATETIME,
> +            PYOBJECT,
>          };
>  
>          virtual enum Type type(V&) = 0;
> @@ -115,6 +117,7 @@
>          virtual ConverterDate date(V&) = 0;
>          virtual ConverterTime time(V&) = 0;
>          virtual ConverterDateTime dateTime(V&) = 0;
> +        virtual PyObjectRef pyObject(V&) = 0;
>  
>          virtual V fromInteger(long long v) = 0;
>          virtual V fromFloating(double v) = 0;
> @@ -123,6 +126,7 @@
>          virtual V fromDate(ConverterDate date) = 0;
>          virtual V fromTime(ConverterTime time) = 0;
>          virtual V fromDateTime(ConverterDateTime dateTime) = 0;
> +        virtual V fromPyObject(const PyObjectRef &pyobj) = 0;
>          virtual ListBuilder<V> *newList() = 0;
>          virtual DictBuilder<V> *newDict() = 0;
>          virtual V none() = 0;
> @@ -187,6 +191,8 @@
>              return tconv.fromTime(fconv.time(from));
>          case FC::DATETIME:
>              return tconv.fromDateTime(fconv.dateTime(from));
> +        case FC::PYOBJECT:
> +            return tconv.fromPyObject(fconv.pyObject(from));
>      }
>  
>      return tconv.none();
> 
> === modified file 'src/pyobject_converter.h'
> --- src/pyobject_converter.h	2014-04-30 15:58:00 +0000
> +++ src/pyobject_converter.h	2015-02-13 14:24:18 +0000
> @@ -24,9 +24,6 @@
>  #include "Python.h"
>  #include "datetime.h"
>  
> -#if PY_MAJOR_VERSION >= 3
> -#  define PY3K
> -#endif
>  
>  class PyObjectListBuilder : public ListBuilder<PyObject *> {
>      public:
> @@ -35,6 +32,7 @@
>  
>          virtual void append(PyObject *o) {
>              PyList_Append(list, o);
> +            Py_DECREF(o);
>          }
>  
>          virtual PyObject * value() {
> @@ -65,35 +63,46 @@
>  
>  class PyObjectListIterator : public ListIterator<PyObject *> {
>      public:
> -        PyObjectListIterator(PyObject *&v) : list(v), pos(0) {}
> -        virtual ~PyObjectListIterator() {}
> -
> -        virtual int count() {
> -            if (PyList_Check(list)) {
> -                return PyList_Size(list);
> -            } else {
> -                return PyTuple_Size(list);
> +        PyObjectListIterator(PyObject *&v)
> +            : list(v)
> +            , iter(PyObject_GetIter(list))
> +            , ref(NULL)
> +        {
> +            if (iter == NULL) {
> +                // TODO: Handle error
> +            }
> +        }
> +
> +        virtual ~PyObjectListIterator()
> +        {
> +            Py_XDECREF(ref);
> +            Py_XDECREF(iter);
> +
> +            if (PyErr_Occurred()) {
> +                // TODO: Handle error
>              }
>          }
>  
>          virtual bool next(PyObject **v) {
> -            if (pos == count()) {
> +            if (!iter) {
>                  return false;
>              }
>  
> -            if (PyList_Check(list)) {
> -                *v = PyList_GetItem(list, pos);
> -            } else {
> -                *v = PyTuple_GetItem(list, pos);
> +            Py_XDECREF(ref);
> +            ref = PyIter_Next(iter);
> +
> +            if (ref) {
> +                *v = ref;
> +                return true;
>              }
>  
> -            pos++;
> -            return true;
> +            return false;
>          }
>  
>      private:
>          PyObject *list;
> -        int pos;
> +        PyObject *iter;
> +        PyObject *ref;
>  };
>  
>  class PyObjectDictIterator : public DictIterator<PyObject *> {
> @@ -131,13 +140,8 @@
>          virtual enum Type type(PyObject *&o) {
>              if (PyBool_Check(o)) {
>                  return BOOLEAN;
> -#ifdef PY3K
>              } else if (PyLong_Check(o)) {
>                  return INTEGER;
> -#else
> -            } else if (PyLong_Check(o) || PyInt_Check(o)) {
> -                return INTEGER;
> -#endif
>              } else if (PyFloat_Check(o)) {
>                  return FLOATING;
>              } else if (PyUnicode_Check(o) || PyBytes_Check(o)) {
> @@ -150,32 +154,18 @@
>                  return DATE;
>              } else if (PyTime_Check(o)) {
>                  return TIME;
> -            } else if (PyList_Check(o) || PyTuple_Check(o)) {
> +            } else if (PyList_Check(o) || PyTuple_Check(o) || PySet_Check(o) || PyIter_Check(o)) {
>                  return LIST;
>              } else if (PyDict_Check(o)) {
>                  return DICT;
>              } else if (o == Py_None) {
>                  return NONE;
> -            }
> -
> -            fprintf(stderr, "Warning: Cannot convert:");
> -            PyObject_Print(o, stderr, 0);
> -            fprintf(stderr, "\n");
> -
> -            return NONE;
> -        }
> -
> -        virtual long long integer(PyObject *&o) {
> -#ifdef PY3K
> -            return PyLong_AsLong(o);
> -#else
> -            if (PyInt_Check(o)) {
> -                return PyInt_AsLong(o);
>              } else {
> -                return PyLong_AsLong(o);
> +                return PYOBJECT;
>              }
> -#endif
>          }
> +
> +        virtual long long integer(PyObject *&o) { return PyLong_AsLong(o); }
>          virtual double floating(PyObject *&o) { return PyFloat_AsDouble(o); }
>          virtual bool boolean(PyObject *&o) { return (o == Py_True); }
>          virtual const char *string(PyObject *&o) {
> @@ -212,6 +202,7 @@
>                      PyDateTime_DATE_GET_SECOND(o),
>                      PyDateTime_DATE_GET_MICROSECOND(o) / 1000);
>          }
> +        virtual PyObjectRef pyObject(PyObject *&o) { return PyObjectRef(o); }
>  
>          virtual PyObject * fromInteger(long long v) { return PyLong_FromLong((long)v); }
>          virtual PyObject * fromFloating(double v) { return PyFloat_FromDouble(v); }
> @@ -222,6 +213,7 @@
>          virtual PyObject * fromDateTime(ConverterDateTime v) {
>              return PyDateTime_FromDateAndTime(v.y, v.m, v.d, v.time.h, v.time.m, v.time.s, v.time.ms * 1000);
>          }
> +        virtual PyObject * fromPyObject(const PyObjectRef &pyobj) { return pyobj.newRef(); }
>          virtual ListBuilder<PyObject *> *newList() { return new PyObjectListBuilder(); }
>          virtual DictBuilder<PyObject *> *newDict() { return new PyObjectDictBuilder(); }
>          virtual PyObject * none() { Py_RETURN_NONE; }
> 
> === added file 'src/pyobject_ref.cpp'
> --- src/pyobject_ref.cpp	1970-01-01 00:00:00 +0000
> +++ src/pyobject_ref.cpp	2015-02-13 14:24:18 +0000
> @@ -0,0 +1,68 @@
> +
> +/**
> + * PyOtherSide: Asynchronous Python 3 Bindings for Qt 5
> + * Copyright (c) 2014, Felix Krull <f_krull at gmx.de>
> + * Copyright (c) 2014, Thomas Perl <m at 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.
> + **/
> +
> +#include "pyobject_ref.h"
> +
> +class EnsureGILState {
> +    public:
> +        EnsureGILState() : gil_state(PyGILState_Ensure()) { }
> +        ~EnsureGILState() { PyGILState_Release(gil_state); }
> +
> +    private:
> +        PyGILState_STATE gil_state;
> +};
> +
> +#define ENSURE_GIL_STATE EnsureGILState _ensure; Q_UNUSED(_ensure)
> +
> +PyObjectRef::PyObjectRef(PyObject *obj)
> +    : pyobject(obj)
> +{
> +    if (pyobject) {
> +        ENSURE_GIL_STATE;
> +        Py_INCREF(pyobject);
> +    }
> +}
> +
> +PyObjectRef::PyObjectRef(const PyObjectRef &other)
> +    : pyobject(other.pyobject)
> +{
> +    if (pyobject) {
> +        ENSURE_GIL_STATE;
> +        Py_INCREF(pyobject);
> +    }
> +}
> +
> +PyObjectRef::~PyObjectRef()
> +{
> +    if (pyobject) {
> +        ENSURE_GIL_STATE;
> +        Py_CLEAR(pyobject);
> +    }
> +}
> +
> +PyObject *
> +PyObjectRef::newRef() const
> +{
> +    if (pyobject) {
> +        ENSURE_GIL_STATE;
> +        Py_INCREF(pyobject);
> +    }
> +
> +    return pyobject;
> +}
> 
> === added file 'src/pyobject_ref.h'
> --- src/pyobject_ref.h	1970-01-01 00:00:00 +0000
> +++ src/pyobject_ref.h	2015-02-13 14:24:18 +0000
> @@ -0,0 +1,41 @@
> +
> +/**
> + * PyOtherSide: Asynchronous Python 3 Bindings for Qt 5
> + * Copyright (c) 2014, Felix Krull <f_krull at gmx.de>
> + * Copyright (c) 2014, Thomas Perl <m at 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.
> + **/
> +
> +#ifndef PYOTHERSIDE_PYOBJECT_REF_H
> +#define PYOTHERSIDE_PYOBJECT_REF_H
> +
> +#include "Python.h"
> +
> +#include <QMetaType>
> +
> +class PyObjectRef {
> +    public:
> +        explicit PyObjectRef(PyObject *obj=0);
> +        PyObjectRef(const PyObjectRef &other);
> +        virtual ~PyObjectRef();
> +
> +        PyObject *newRef() const;
> +
> +    private:
> +        PyObject *pyobject;
> +};
> +
> +Q_DECLARE_METATYPE(PyObjectRef)
> +
> +#endif // PYOTHERSIDE_PYOBJECT_REF_H
> 
> === modified file 'src/pyotherside_plugin.cpp'
> --- src/pyotherside_plugin.cpp	2014-04-30 15:58:00 +0000
> +++ src/pyotherside_plugin.cpp	2015-02-13 14:24:18 +0000
> @@ -60,4 +60,5 @@
>      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);
>  }
> 
> === modified file 'src/qpython.cpp'
> --- src/qpython.cpp	2014-04-30 15:58:00 +0000
> +++ src/qpython.cpp	2015-02-13 14:24:18 +0000
> @@ -53,6 +53,8 @@
>  
>      QObject::connect(this, SIGNAL(process(QString,QVariant,QJSValue *)),
>                       worker, SLOT(process(QString,QVariant,QJSValue *)));
> +    QObject::connect(this, SIGNAL(processMethod(QVariant,QString,QVariant,QJSValue *)),
> +                     worker, SLOT(processMethod(QVariant,QString,QVariant,QJSValue *)));
>      QObject::connect(worker, SIGNAL(finished(QVariant,QJSValue *)),
>                       this, SLOT(finished(QVariant,QJSValue *)));
>  
> @@ -80,7 +82,22 @@
>  
>      // Strip leading "file://" (for use with Qt.resolvedUrl())
>      if (path.startsWith("file://")) {
> +#ifdef WIN32
> +        // On Windows, path would be "file:///C:\...", so strip 8 chars to get
> +        // a Windows-compatible absolute filename to be used as import path
> +        path = path.mid(8);
> +#else
>          path = path.mid(7);
> +#endif
> +    }
> +
> +    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();
> @@ -146,6 +163,7 @@
>      }
>  
>      PyDict_SetItemString(priv->globals, moduleName, module);
> +    Py_CLEAR(module);
>      priv->leave();
>      return true;
>  }
> @@ -229,40 +247,89 @@
>          return QVariant();
>      }
>  
> -    if (PyCallable_Check(callable)) {
> -        QVariant v;
> -
> -        PyObject *argl = convertQVariantToPyObject(args);
> -        if (!PyList_Check(argl)) {
> -            Py_DECREF(callable);
> -            Py_XDECREF(argl);
> -            emit error(QString("Not a parameter list in call to %1: %2")
> -                    .arg(func).arg(args.toString()));
> -            priv->leave();
> -            return QVariant();
> -        }
> -
> -        PyObject *argt = PyList_AsTuple(argl);
> -        Py_DECREF(argl);
> -        PyObject *o = PyObject_Call(callable, argt, NULL);
> -        Py_DECREF(argt);
> -
> -        if (o == NULL) {
> -            emit error(QString("Return value of PyObject call is NULL: %1").arg(priv->formatExc()));
> -        } else {
> -            v = convertPyObjectToQVariant(o);
> -            Py_DECREF(o);
> -        }
> -
> -        Py_DECREF(callable);
> -        priv->leave();
> -        return v;
> -    }
> -
> -    emit error(QString("Not a callable: %1").arg(func));
> -    Py_DECREF(callable);
> -    priv->leave();
> -    return QVariant();
> +    QVariant v;
> +    QString errorMessage = priv->call(callable, func, args, &v);
> +    if (!errorMessage.isNull()) {
> +        emit error(errorMessage);
> +    }
> +    Py_DECREF(callable);
> +    priv->leave();
> +    return v;
> +}
> +
> +void
> +QPython::callMethod(QVariant obj, QString method, QVariant args, QJSValue callback)
> +{
> +    QJSValue *cb = 0;
> +    if (!callback.isNull() && !callback.isUndefined() && callback.isCallable()) {
> +        cb = new QJSValue(callback);
> +    }
> +    emit processMethod(obj, method, args, cb);
> +}
> +
> +QVariant
> +QPython::callMethod_sync(QVariant obj, QString method, QVariant args)
> +{
> +    priv->enter();
> +    PyObject *pyobj = convertQVariantToPyObject(obj);
> +
> +    if (pyobj == NULL) {
> +        emit error(QString("Failed to convert %1 to python object: '%1' (%2)").arg(obj.toString()).arg(priv->formatExc()));
> +        priv->leave();
> +        return QVariant();
> +    }
> +
> +    QByteArray byteArray = method.toUtf8();
> +    const char *methodStr = byteArray.data();
> +
> +    PyObject *callable = PyObject_GetAttrString(pyobj, methodStr);
> +
> +    if (callable == NULL) {
> +        emit error(QString("Method not found: '%1' (%2)").arg(method).arg(priv->formatExc()));
> +        Py_DECREF(pyobj);
> +        priv->leave();
> +        return QVariant();
> +    }
> +
> +    QVariant v;
> +    QString errorMessage = priv->call(callable, method, args, &v);
> +    if (!errorMessage.isNull()) {
> +        emit error(errorMessage);
> +    }
> +    Py_DECREF(callable);
> +    Py_DECREF(pyobj);
> +    priv->leave();
> +    return v;
> +}
> +
> +QVariant
> +QPython::getattr(QVariant obj, QString attr) {
> +    priv->enter();
> +    PyObject *pyobj = convertQVariantToPyObject(obj);
> +
> +    if (pyobj == NULL) {
> +        emit error(QString("Failed to convert %1 to python object: '%1' (%2)").arg(obj.toString()).arg(priv->formatExc()));
> +        priv->leave();
> +        return QVariant();
> +    }
> +
> +    QByteArray byteArray = attr.toUtf8();
> +    const char *attrStr = byteArray.data();
> +
> +    PyObject *o = PyObject_GetAttrString(pyobj, attrStr);
> +
> +    if (o == NULL) {
> +        emit error(QString("Attribute not found: '%1' (%2)").arg(attr).arg(priv->formatExc()));
> +        Py_DECREF(pyobj);
> +        priv->leave();
> +        return QVariant();
> +    }
> +
> +    QVariant v = convertPyObjectToQVariant(o);
> +    Py_DECREF(o);
> +    Py_DECREF(pyobj);
> +    priv->leave();
> +    return v;
>  }
>  
>  void
> 
> === modified file 'src/qpython.h'
> --- src/qpython.h	2014-04-30 15:58:00 +0000
> +++ src/qpython.h	2015-02-13 14:24:18 +0000
> @@ -19,6 +19,8 @@
>  #ifndef PYOTHERSIDE_QPYTHON_H
>  #define PYOTHERSIDE_QPYTHON_H
>  
> +#include "Python.h"
> +
>  #include <QVariant>
>  #include <QObject>
>  #include <QString>
> @@ -240,6 +242,95 @@
>  
>  
>          /**
> +         * \brief Asynchronously call a Python method
> +         *
> +         * Call a method of a Python object asynchronously and call back
> +         * into QML when the result is available:
> +         *
> +         * \code
> +         * Python {
> +         *     Component.onCompleted: {
> +         *         importModule('datetime', function() {
> +         *             call('datetime.datetime.now', [], function(dt) {
> +         *                 console.log(dt);
> +         *                 callMethod(dt, 'strftime', ['%Y-%m-%d'], function(result) {
> +         *                     console.log(result);
> +         *                 });
> +         *             });
> +         *         });
> +         *     }
> +         * }
> +         * \endcode
> +         *
> +         * \arg obj The Python object
> +         * \arg method The method to call
> +         * \arg args A list of arguments, or \c [] for no arguments
> +         * \arg callback A callback that receives the function call result
> +         **/
> +        Q_INVOKABLE void
> +        callMethod(
> +            QVariant obj,
> +            QString func,
> +            QVariant args=QVariantList(),
> +            QJSValue callback=QJSValue());
> +
> +
> +        /**
> +         * \brief Synchronously call a Python method
> +         *
> +         * This is the synchronous variant of callMethod(). In general, you
> +         * should use callMethod() instead of this function to avoid blocking
> +         * the QML UI thread. Example usage:
> +         *
> +         * \code
> +         * Python {
> +         *     Component.onCompleted: {
> +         *         importModule('datetime', function() {
> +         *             call('datetime.datetime.now', [], function(dt) {
> +         *                 console.log(dt);
> +         *                 console.log(
> +         *                     callMethod_sync(dt, 'strftime', ['%Y-%m-%d'])
> +         *                 );
> +         *             });
> +         *         });
> +         *     }
> +         * }
> +         * \endcode
> +         *
> +         * \arg obj The Python object
> +         * \arg method The method to call
> +         * \arg args A list of arguments, or \c [] for no arguments
> +         * \result The return value of the Python call as Qt data type
> +         **/
> +        Q_INVOKABLE QVariant
> +        callMethod_sync(
> +            QVariant obj,
> +            QString func,
> +            QVariant args=QVariantList());
> +
> +        /**
> +         * \brief Get an attribute value of a Python object synchronously
> +         *
> +         * \code
> +         * Python {
> +         *     Component.onCompleted: {
> +         *         importModule('datetime', function() {
> +         *             call('datetime.datetime.now', [], function(dt) {
> +         *                 console.log('Year: ' + getattr(dt, 'year'));
> +         *             });
> +         *         });
> +         *     }
> +         * }
> +         * \endcode
> +         *
> +         * \arg obj The Python object
> +         * \arg attr The attribute to get
> +         * \result The attribute value
> +         **/
> +        Q_INVOKABLE QVariant
> +        getattr(QVariant obj, QString attr);
> +
> +        /**
>           * \brief Get the PyOtherSide version
>           *
>           * \result The running version of PyOtherSide
> @@ -280,6 +371,7 @@
>  
>          /* For internal use only */
>          void process(QString func, QVariant args, QJSValue *callback);
> +        void processMethod(QVariant obj, QString method, QVariant args, QJSValue *callback);
>          void import(QString name, QJSValue *callback);
>  
>      private slots:
> @@ -317,4 +409,13 @@
>      }
>  };
>  
> +class QPython13 : public QPython {
> +Q_OBJECT
> +public:
> +    QPython13(QObject *parent=0)
> +        : QPython(parent, 1, 3)
> +    {
> +    }
> +};
> +
>  #endif /* PYOTHERSIDE_QPYTHON_H */
> 
> === modified file 'src/qpython_priv.cpp'
> --- src/qpython_priv.cpp	2014-04-30 15:58:00 +0000
> +++ src/qpython_priv.cpp	2015-02-13 14:24:18 +0000
> @@ -22,9 +22,25 @@
>  
>  #include <QImage>
>  #include <QDebug>
> +#include <QResource>
> +#include <QFile>
> +#include <QDir>
>  
>  static QPythonPriv *priv = NULL;
>  
> +static QString
> +qstring_from_pyobject_arg(PyObject *object)
> +{
> +    PyObjectConverter conv;
> +
> +    if (conv.type(object) != PyObjectConverter::STRING) {
> +        PyErr_SetString(PyExc_ValueError, "Argument must be a string");
> +        return QString();
> +    }
> +
> +    return QString::fromUtf8(conv.string(object));
> +}
> +
>  PyObject *
>  pyotherside_send(PyObject *self, PyObject *args)
>  {
> @@ -58,14 +74,93 @@
>      Py_RETURN_NONE;
>  }
>  
> +PyObject *
> +pyotherside_qrc_is_file(PyObject *self, PyObject *filename)
> +{
> +    QString qfilename = qstring_from_pyobject_arg(filename);
> +
> +    if (qfilename.isNull()) {
> +        return NULL;
> +    }
> +
> +    if (QFile(":" + qfilename).exists()) {
> +        Py_RETURN_TRUE;
> +    }
> +
> +    Py_RETURN_FALSE;
> +}
> +
> +PyObject *
> +pyotherside_qrc_is_dir(PyObject *self, PyObject *dirname)
> +{
> +    QString qdirname = qstring_from_pyobject_arg(dirname);
> +
> +    if (qdirname.isNull()) {
> +        return NULL;
> +    }
> +
> +    if (QDir(":" + qdirname).exists()) {
> +        Py_RETURN_TRUE;
> +    }
> +
> +    Py_RETURN_FALSE;
> +}
> +
> +PyObject *
> +pyotherside_qrc_get_file_contents(PyObject *self, PyObject *filename)
> +{
> +    QString qfilename = qstring_from_pyobject_arg(filename);
> +
> +    if (qfilename.isNull()) {
> +        return NULL;
> +    }
> +
> +    QFile file(":" + qfilename);
> +    if (!file.exists() || !file.open(QIODevice::ReadOnly)) {
> +        PyErr_SetString(PyExc_ValueError, "File not found");
> +        return NULL;
> +    }
> +
> +    QByteArray ba = file.readAll();
> +    return PyByteArray_FromStringAndSize(ba.constData(), ba.size());
> +}
> +
> +PyObject *
> +pyotherside_qrc_list_dir(PyObject *self, PyObject *dirname)
> +{
> +    QString qdirname = qstring_from_pyobject_arg(dirname);
> +
> +    if (qdirname.isNull()) {
> +        return NULL;
> +    }
> +
> +    QDir dir(":" + qdirname);
> +    if (!dir.exists()) {
> +        PyErr_SetString(PyExc_ValueError, "Directory not found");
> +        return NULL;
> +    }
> +
> +    return convertQVariantToPyObject(dir.entryList());
> +}
> +
>  static PyMethodDef PyOtherSideMethods[] = {
> +    /* Introduced in PyOtherSide 1.0 */
>      {"send", pyotherside_send, METH_VARARGS, "Send data to Qt."},
>      {"atexit", pyotherside_atexit, METH_O, "Function to call on shutdown."},
> +
> +    /* Introduced in PyOtherSide 1.1 */
>      {"set_image_provider", pyotherside_set_image_provider, METH_O, "Set the QML image provider."},
> +
> +    /* Introduced in PyOtherSide 1.3 */
> +    {"qrc_is_file", pyotherside_qrc_is_file, METH_O, "Check if a file exists in Qt Resources."},
> +    {"qrc_is_dir", pyotherside_qrc_is_dir, METH_O, "Check if a directory exists in Qt Resources."},
> +    {"qrc_get_file_contents", pyotherside_qrc_get_file_contents, METH_O, "Get file contents from a Qt Resource."},
> +    {"qrc_list_dir", pyotherside_qrc_list_dir, METH_O, "Get directory entries from a Qt Resource."},
> +
> +    /* sentinel */
>      {NULL, NULL, 0, NULL},
>  };
>  
> -#ifdef PY3K
>  static struct PyModuleDef PyOtherSideModule = {
>      PyModuleDef_HEAD_INIT,
>      "pyotherside",   /* name of module */
> @@ -94,22 +189,21 @@
>      // Custom constant - pixels are to be interpreted as encoded image file data
>      PyModule_AddIntConstant(pyotherside, "format_data", -1);
>  
> +    // Version of PyOtherSide (new in 1.3)
> +    PyModule_AddStringConstant(pyotherside, "version", PYOTHERSIDE_VERSION);
> +
>      return pyotherside;
>  }
> -#endif
>  
>  QPythonPriv::QPythonPriv()
>      : locals(NULL)
>      , globals(NULL)
> -    , state(NULL)
> +    , gil_state()
>      , atexit_callback(NULL)
>      , image_provider(NULL)
>      , traceback_mod(NULL)
> -    , mutex()
>  {
> -#ifdef PY3K
>      PyImport_AppendInittab("pyotherside", PyOtherSide_init);
> -#endif
>  
>      Py_Initialize();
>      PyEval_InitThreads();
> @@ -123,10 +217,6 @@
>      traceback_mod = PyImport_ImportModule("traceback");
>      assert(traceback_mod != NULL);
>  
> -#ifndef PY3K
> -    Py_InitModule("pyotherside", PyOtherSideMethods);
> -#endif
> -
>      priv = this;
>  
>      if (PyDict_GetItemString(globals, "__builtins__") == NULL) {
> @@ -134,10 +224,6 @@
>                  PyEval_GetBuiltins());
>      }
>  
> -    // Need to lock mutex here, as it will always be unlocked
> -    // by leave(). If we don't do that, it will be unlocked
> -    // once too often resulting in undefined behavior.
> -    mutex.lock();
>      leave();
>  }
>  
> @@ -153,18 +239,13 @@
>  void
>  QPythonPriv::enter()
>  {
> -    mutex.lock();
> -    assert(state != NULL);
> -    PyEval_RestoreThread(state);
> -    state = NULL;
> +    gil_state = PyGILState_Ensure();
>  }
>  
>  void
>  QPythonPriv::leave()
>  {
> -    assert(state == NULL);
> -    state = PyEval_SaveThread();
> -    mutex.unlock();
> +    PyGILState_Release(gil_state);
>  }
>  
>  void
> @@ -283,3 +364,79 @@
>  {
>      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();
> +}
> +
> +QString
> +QPythonPriv::call(PyObject *callable, QString name, QVariant args, QVariant *v)
> +{
> +    if (!PyCallable_Check(callable)) {
> +        return QString("Not a callable: %1").arg(name);
> +    }
> +
> +    PyObject *argl = convertQVariantToPyObject(args);
> +    if (!PyList_Check(argl)) {
> +        Py_XDECREF(argl);
> +        return QString("Not a parameter list in call to %1: %2")
> +                .arg(name).arg(args.toString());
> +    }
> +
> +    PyObject *argt = PyList_AsTuple(argl);
> +    Py_DECREF(argl);
> +    PyObject *o = PyObject_Call(callable, argt, NULL);
> +    Py_DECREF(argt);
> +
> +    if (o == NULL) {
> +        return QString("Return value of PyObject call is NULL: %1").arg(priv->formatExc());
> +    } else {
> +        if (v != NULL) {
> +            *v = convertPyObjectToQVariant(o);
> +        }
> +        Py_DECREF(o);
> +    }
> +    return QString();
> +}
> 
> === modified file 'src/qpython_priv.h'
> --- src/qpython_priv.h	2014-04-30 15:58:00 +0000
> +++ src/qpython_priv.h	2015-02-13 14:24:18 +0000
> @@ -24,7 +24,6 @@
>  #include <QObject>
>  #include <QVariant>
>  #include <QString>
> -#include <QMutex>
>  
>  class QPythonPriv : public QObject {
>      Q_OBJECT
> @@ -38,6 +37,9 @@
>          void enter();
>          void leave();
>  
> +        QString importFromQRC(const char *module, const QString &filename);
> +        QString call(PyObject *callable, QString name, QVariant args, QVariant *v);
> +
>          void receiveObject(PyObject *o);
>          static void closing();
>          static QPythonPriv *instance();
> @@ -46,13 +48,11 @@
>  
>          PyObject *locals;
>          PyObject *globals;
> -        PyThreadState *state;
> +        PyGILState_STATE gil_state;
>          PyObject *atexit_callback;
>          PyObject *image_provider;
>          PyObject *traceback_mod;
>  
> -        QMutex mutex;
> -
>      signals:
>          void receive(QVariant data);
>  };
> 
> === modified file 'src/qpython_worker.cpp'
> --- src/qpython_worker.cpp	2014-04-30 15:58:00 +0000
> +++ src/qpython_worker.cpp	2015-02-13 14:24:18 +0000
> @@ -41,6 +41,15 @@
>  }
>  
>  void
> +QPythonWorker::processMethod(QVariant obj, QString method, QVariant args, QJSValue *callback)
> +{
> +    QVariant result = qpython->callMethod_sync(obj, method, args);
> +    if (callback) {
> +        emit finished(result, callback);
> +    }
> +}
> +
> +void
>  QPythonWorker::import(QString name, QJSValue *callback)
>  {
>      bool result = qpython->importModule_sync(name);
> 
> === modified file 'src/qpython_worker.h'
> --- src/qpython_worker.h	2014-04-30 15:58:00 +0000
> +++ src/qpython_worker.h	2015-02-13 14:24:18 +0000
> @@ -35,6 +35,7 @@
>  
>      public slots:
>          void process(QString func, QVariant args, QJSValue *callback);
> +        void processMethod(QVariant obj, QString method, QVariant args, QJSValue *callback);
>          void import(QString func, QJSValue *callback);
>  
>      signals:
> 
> === added file 'src/qrc_importer.py'
> --- src/qrc_importer.py	1970-01-01 00:00:00 +0000
> +++ src/qrc_importer.py	2015-02-13 14:24:18 +0000
> @@ -0,0 +1,47 @@
> +#
> +# PyOtherSide: Asynchronous Python 3 Bindings for Qt 5
> +# Copyright (c) 2014, Thomas Perl <m at 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())
> 
> === added file 'src/qrc_importer.qrc'
> --- src/qrc_importer.qrc	1970-01-01 00:00:00 +0000
> +++ src/qrc_importer.qrc	2015-02-13 14:24:18 +0000
> @@ -0,0 +1,6 @@
> +<!DOCTYPE RCC>
> +<RCC version="1.0">
> +  <qresource prefix="/io/thp/pyotherside/">
> +    <file>qrc_importer.py</file>
> +  </qresource>
> +</RCC>
> 
> === modified file 'src/qvariant_converter.h'
> --- src/qvariant_converter.h	2014-04-30 15:58:00 +0000
> +++ src/qvariant_converter.h	2015-02-13 14:24:18 +0000
> @@ -66,12 +66,8 @@
>          QVariantListIterator(QVariant &v) : list(v.toList()), pos(0) {}
>          virtual ~QVariantListIterator() {}
>  
> -        virtual int count() {
> -            return list.size();
> -        }
> -
>          virtual bool next(QVariant *v) {
> -            if (pos == count()) {
> +            if (pos == list.size()) {
>                  return false;
>              }
>  
> @@ -116,34 +112,40 @@
>          virtual ~QVariantConverter() {}
>  
>          virtual enum Type type(QVariant &v) {
> -            QVariant::Type t = v.type();
> +            QMetaType::Type t = (QMetaType::Type)v.type();
>              switch (t) {
> -                case QVariant::Bool:
> +                case QMetaType::Bool:
>                      return BOOLEAN;
> -                case QVariant::Int:
> -                case QVariant::LongLong:
> -                case QVariant::UInt:
> -                case QVariant::ULongLong:
> +                case QMetaType::Int:
> +                case QMetaType::LongLong:
> +                case QMetaType::UInt:
> +                case QMetaType::ULongLong:
>                      return INTEGER;
> -                case QVariant::Double:
> +                case QMetaType::Double:
>                      return FLOATING;
> -                case QVariant::String:
> +                case QMetaType::QString:
>                      return STRING;
> -                case QVariant::Date:
> +                case QMetaType::QDate:
>                      return DATE;
> -                case QVariant::Time:
> +                case QMetaType::QTime:
>                      return TIME;
> -                case QVariant::DateTime:
> +                case QMetaType::QDateTime:
>                      return DATETIME;
> -                case QVariant::List:
> +                case QMetaType::QVariantList:
> +                case QMetaType::QStringList:
>                      return LIST;
> -                case QVariant::Map:
> +                case QMetaType::QVariantMap:
>                      return DICT;
> -                case QVariant::Invalid:
> +                case QMetaType::UnknownType:
>                      return NONE;
>                  default:
> -                    qDebug() << "Cannot convert:" << v;
> -                    return NONE;
> +                    int userType = v.userType();
> +                    if (userType == qMetaTypeId<PyObjectRef>()) {
> +                        return PYOBJECT;
> +                    } else {
> +                        qDebug() << "Cannot convert:" << v;
> +                        return NONE;
> +                    }
>              }
>          }
>  
> @@ -190,6 +192,10 @@
>              return stringstorage.constData();
>          }
>  
> +        virtual PyObjectRef pyObject(QVariant &v) {
> +            return v.value<PyObjectRef>();
> +        }
> +
>          virtual ListBuilder<QVariant> *newList() {
>              return new QVariantListBuilder;
>          }
> @@ -209,6 +215,9 @@
>              QTime t(v.time.h, v.time.m, v.time.s, v.time.ms);
>              return QVariant(QDateTime(d, t));
>          }
> +        virtual QVariant fromPyObject(const PyObjectRef &pyobj) {
> +            return QVariant::fromValue(pyobj);
> +        }
>          virtual QVariant none() { return QVariant(); };
>  
>      private:
> 
> === modified file 'src/src.pro'
> --- src/src.pro	2014-04-30 15:58:00 +0000
> +++ src/src.pro	2015-02-13 14:24:18 +0000
> @@ -28,6 +28,9 @@
>  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
> @@ -40,6 +43,10 @@
>  SOURCES += global_libpython_loader.cpp
>  HEADERS += global_libpython_loader.h
>  
> +# Reference-counting PyObject wrapper class
> +SOURCES += pyobject_ref.cpp
> +HEADERS += pyobject_ref.h
> +
>  # Type System Conversion Logic
>  HEADERS += converter.h
>  HEADERS += qvariant_converter.h
> 
> === added directory 'tests/test_iterable'
> === added file 'tests/test_iterable/test_iterable.py'
> --- tests/test_iterable/test_iterable.py	1970-01-01 00:00:00 +0000
> +++ tests/test_iterable/test_iterable.py	2015-02-13 14:24:18 +0000
> @@ -0,0 +1,9 @@
> +def get_set():
> +    return set((1, 2, 3))
> +
> +def get_iterable_generator_expression():
> +    return (x * 2 for x in range(4))
> +
> +def get_iterable_generator():
> +    for i in range(5):
> +        yield i * 3
> 
> === added file 'tests/test_iterable/test_iterable.qml'
> --- tests/test_iterable/test_iterable.qml	1970-01-01 00:00:00 +0000
> +++ tests/test_iterable/test_iterable.qml	2015-02-13 14:24:18 +0000
> @@ -0,0 +1,64 @@
> +import QtQuick 2.0
> +import io.thp.pyotherside 1.3
> +
> +Python {
> +    property var tests: ([])
> +
> +    Component.onCompleted: {
> +        addImportPath(Qt.resolvedUrl('.'));
> +
> +        importModule('test_iterable', function () {
> +            function test_next() {
> +                console.log('================================');
> +                if (tests.length == 0) {
> +                    console.log('Tests completed');
> +                    Qt.quit();
> +                } else {
> +                    var test = tests.pop();
> +                    console.log('-> ' + test.name);
> +                    call(test.func, [], function (reply) {
> +                        if (reply === undefined || reply === null) {
> +                            error('Got undefined or null');
> +                            return;
> +                        }
> +
> +                        // Sort, because a Python set is unordered (to make expected work below)
> +                        reply.sort(function (a, b) { return a - b; });
> +
> +                        console.log('Got:      ' + reply);
> +                        console.log('Expected: ' + test.expected);
> +                        if (reply.toString() !== test.expected.toString()) {
> +                            error('Results do not match');
> +                            return;
> +                        }
> +                        test_next();
> +                    });
> +                }
> +            }
> +
> +            tests.unshift({
> +                name: 'Getting set returns JS array',
> +                func: 'test_iterable.get_set',
> +                expected: [1, 2, 3]
> +            });
> +            tests.unshift({
> +                name: 'Getting generator expression returns JS array',
> +                func: 'test_iterable.get_iterable_generator_expression',
> +                expected: [0, 2, 4, 6]
> +            });
> +            tests.unshift({
> +                name: 'Getting generator returns JS array',
> +                func: 'test_iterable.get_iterable_generator',
> +                expected: [0, 3, 6, 9, 12]
> +            });
> +
> +            test_next();
> +        });
> +    }
> +
> +    onError: {
> +        console.log('Error: ' + traceback);
> +        console.log('Tests failed');
> +        Qt.quit();
> +    }
> +}
> 
> === added directory 'tests/test_wrapped'
> === added file 'tests/test_wrapped/test_wrapped.py'
> --- tests/test_wrapped/test_wrapped.py	1970-01-01 00:00:00 +0000
> +++ tests/test_wrapped/test_wrapped.py	2015-02-13 14:24:18 +0000
> @@ -0,0 +1,23 @@
> +# -*- coding: utf-8 -*-
> +
> +class Foo(object):
> +    def __init__(self, name):
> +        print('new Foo(', name, ')')
> +        self.name = name
> +        self.bar = 4711
> +
> +    def methodman(self, something):
> +        return 'I came to bring {}'.format(something)
> +
> +    def __del__(self):
> +        print('__del__ called on', self.name)
> +
> +
> +def get_foo():
> +    print('get_foo()')
> +    return Foo('Hello World!')
> +
> +def set_foo(foo):
> +    print('set_foo(', foo, ')')
> +    return foo.name
> +
> 
> === added file 'tests/test_wrapped/test_wrapped.qml'
> --- tests/test_wrapped/test_wrapped.qml	1970-01-01 00:00:00 +0000
> +++ tests/test_wrapped/test_wrapped.qml	2015-02-13 14:24:18 +0000
> @@ -0,0 +1,34 @@
> +import QtQuick 2.0
> +import io.thp.pyotherside 1.0
> +
> +Rectangle {
> +    id: page
> +    width: 300
> +    height: 300
> +
> +    Python {
> +        Component.onCompleted: {
> +            addImportPath(Qt.resolvedUrl('.'));
> +
> +            importModule('test_wrapped', function () {
> +                console.log('"test_wrapped" imported successfully');
> +
> +                var foo = call_sync('test_wrapped.get_foo', []);
> +                console.log('got foo: ' + foo);
> +
> +                console.log('attribute bar of foo: ' + getattr(foo, 'bar'));
> +
> +                callMethod(foo, 'methodman', ['the pain'], function (result) {
> +                    console.log('methodman() result: ' + result);
> +                });
> +
> +                var result = call_sync('test_wrapped.set_foo', [foo]);
> +                console.log('got result: ' + result);
> +            });
> +        }
> +
> +        onError: {
> +            console.log('Received error: ' + traceback);
> +        }
> +    }
> +}
> 
> === modified file 'tests/tests.cpp'
> --- tests/tests.cpp	2014-04-30 15:58:00 +0000
> +++ tests/tests.cpp	2015-02-13 14:24:18 +0000
> @@ -71,7 +71,6 @@
>      v = builder->value();
>      delete builder;
>      ListIterator<V> *iterator = conv->list(v);
> -    QVERIFY(iterator->count() == 2);
>      QVERIFY(iterator->next(&w));
>      QVERIFY(conv->type(w) == Converter<V>::INTEGER);
>      QVERIFY(conv->integer(w) == 444);
> @@ -94,9 +93,68 @@
>      QVERIFY(conv->boolean(x) == true);
>      delete iterator2;
>  
> +    /* Convert from/to generic PyObject */
> +    PyObject *obj = PyCapsule_New(conv, "test", NULL);
> +    v = conv->fromPyObject(PyObjectRef(obj));
> +    QVERIFY(conv->type(v) == Converter<V>::PYOBJECT);
> +
> +    // Check if getting a new reference works
> +    PyObject *o = conv->pyObject(v).newRef();
> +    QVERIFY(o == obj);
> +    Py_DECREF(o);
> +
> +    Py_CLEAR(obj);
> +
>      delete conv;
>  }
>  
> +void destruct(PyObject *obj) {
> +    bool *destructor_called = (bool *)PyCapsule_GetPointer(obj, "test");
> +    *destructor_called = true;
> +}
> +
> +void
> +TestPyOtherSide::testPyObjectRefRoundTrip()
> +{
> +    // Simulate a complete round-trip of a PyObject reference, from PyOtherSide
> +    // to QML and back.
> +
> +    // Create a Python object, i.e. in a Python function.
> +    bool destructor_called = false;
> +    PyObject *o = PyCapsule_New(&destructor_called, "test", destruct);
> +    QVERIFY(o->ob_refcnt == 1);
> +
> +    // Convert the object to a QVariant and increment its refcount.
> +    QVariant v = convertPyObjectToQVariant(o);
> +
> +    // Decrement refcount and pass QVariant to QML.
> +    QVERIFY(o->ob_refcnt == 2);
> +    Py_DECREF(o);
> +    QVERIFY(o->ob_refcnt == 1);
> +
> +    // Pass QVariant back to PyOtherSide, which converts it to a PyObject,
> +    // incrementing its refcount.
> +    PyObject *o2 = convertQVariantToPyObject(v);
> +    QVERIFY(o->ob_refcnt == 2);
> +
> +    // The QVariant is deleted, i.e. by a JS variable falling out of scope.
> +    // This deletes the PyObjectRef and thus decrements the object's refcount.
> +    v = QVariant();
> +
> +    // At this point, we only have one reference (the one from o2)
> +    QVERIFY(o->ob_refcnt == 1);
> +
> +    // There's still a reference, so the destructor must not have been called
> +    QVERIFY(!destructor_called);
> +
> +    // Now, at this point, the last remaining reference is removed, which
> +    // will cause the destructor to be called
> +    Py_DECREF(o2);
> +
> +    // There are no references left, so the capsule's destructor is called.
> +    QVERIFY(destructor_called);
> +}
> +
>  void
>  TestPyOtherSide::testQVariantConverter()
>  {
> @@ -153,4 +211,46 @@
>      // PyOtherSide API 1.2
>      QPython12 py12;
>      testEvaluateWith(&py12);
> +
> +    // PyOtherSide API 1.3
> +    QPython13 py13;
> +    testEvaluateWith(&py13);
> +}
> +
> +void
> +TestPyOtherSide::testSetToList()
> +{
> +    // Test if a Python set is converted to a list
> +    PyObject *set = PySet_New(NULL);
> +    QVERIFY(set != NULL);
> +    PyObject *o = NULL;
> +
> +    o = PyLong_FromLong(123);
> +    QVERIFY(o != NULL);
> +    QVERIFY(PySet_Add(set, o) == 0);
> +
> +    o = PyLong_FromLong(321);
> +    QVERIFY(o != NULL);
> +    QVERIFY(PySet_Add(set, o) == 0);
> +
> +    o = PyLong_FromLong(444);
> +    QVERIFY(o != NULL);
> +    QVERIFY(PySet_Add(set, o) == 0);
> +
> +    // This will not be added (no duplicates in a set)
> +    o = PyLong_FromLong(123);
> +    QVERIFY(o != NULL);
> +    QVERIFY(PySet_Add(set, o) == 0);
> +
> +    // At this point, we should have 3 items (123, 321 and 444)
> +    QVERIFY(PySet_Size(set) == 3);
> +
> +    QVariant v = convertPyObjectToQVariant(set);
> +    QVERIFY(v.canConvert(QMetaType::QVariantList));
> +
> +    QList<QVariant> l = v.toList();
> +    QVERIFY(l.size() == 3);
> +    QVERIFY(l.contains(123));
> +    QVERIFY(l.contains(321));
> +    QVERIFY(l.contains(444));
>  }
> 
> === modified file 'tests/tests.h'
> --- tests/tests.h	2014-04-30 15:58:00 +0000
> +++ tests/tests.h	2015-02-13 14:24:18 +0000
> @@ -34,7 +34,9 @@
>          void testEvaluate();
>          void testQVariantConverter();
>          void testPyObjectConverter();
> +        void testPyObjectRefRoundTrip();
>          void testConvertToPythonAndBack();
> +        void testSetToList();
>  };
>  
>  #endif /* PYOTHERSIDE_TESTS_H */
> 
> === modified file 'tests/tests.pro'
> --- tests/tests.pro	2014-04-30 15:58:00 +0000
> +++ tests/tests.pro	2015-02-13 14:24:18 +0000
> @@ -10,6 +10,7 @@
>  SOURCES += ../src/qpython.cpp
>  SOURCES += ../src/qpython_worker.cpp
>  SOURCES += ../src/qpython_priv.cpp
> +SOURCES += ../src/pyobject_ref.cpp
>  
>  HEADERS += ../src/qpython.h
>  HEADERS += ../src/qpython_worker.h
> 


-- 
https://code.launchpad.net/~3v1n0/ubuntu/vivid/pyotherside/bump-to-1.4.0-git20150111/+merge/249655
Your team Ubuntu branches is subscribed to branch lp:ubuntu/pyotherside.



More information about the Ubuntu-reviews mailing list