[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