[Merge] lp:~3v1n0/ubuntu/vivid/pyotherside/bump-to-1.4.0-git20150111 into lp:ubuntu/pyotherside
Zygmunt Krynicki
zygmunt.krynicki at canonical.com
Fri Feb 13 14:34:18 UTC 2015
Review: Needs Information
Hey.
Thanks for proposing this. It looks okay though I'd like to know how to actually get this into Ubuntu as in the past it would just sync from Debian. I cannot upload to Ubuntu (nor Debian) directly so I'm not sure how to proceed.
Oh, and one more question below (inline)
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
Is python3.2 unsupported now?
> 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