Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement wrapInstance and getCppPointer (#59) #190

Merged
merged 7 commits into from
Jun 27, 2017
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Dockerfile-py2.7
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ RUN apt-get update && \
software-properties-common && \
add-apt-repository -y ppa:thopiekar/pyside-git && \
apt-get update && apt-get install -y \
nano \
python \
python-dev \
python-pip \
Expand All @@ -16,8 +17,21 @@ RUN apt-get update && \
python-pyside \
python-pyside2 \
pyside2-tools \
libshiboken2-dev \
xvfb

# Build shiboken
RUN apt-get install -y \
wget \
libqt4-dev \
cmake && \
wget https://pypi.python.org/packages/source/S/Shiboken/Shiboken-1.2.2.tar.gz && \
tar -xvzf Shiboken-1.2.2.tar.gz && \
cd Shiboken-1.2.2 && \
python setup.py bdist_wheel --qmake=/usr/bin/qmake-qt4 && \
pip install dist/Shiboken-1.2.2-cp27-cp27mu-linux_x86_64.whl && \
python shiboken_postinstall.py -install

# Make pyside2uic availble for Python 2.x
RUN cp -avr /usr/lib/python3/dist-packages/pyside2uic /usr/local/lib/python2.7/dist-packages

Expand All @@ -32,6 +46,7 @@ ENV QT_TESTING true
ENV DISPLAY :99

# Warnings are exceptions
ENV PYTHONPATH=/Shiboken-1.2.2/shiboken_package/Shiboken
ENV PYTHONWARNINGS="ignore"

WORKDIR /workspace/Qt.py
Expand Down
5 changes: 5 additions & 0 deletions Dockerfile-py3.5
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@ FROM ubuntu:16.04
# Needed to install pyside2-tools without issues
ENV DEBIAN_FRONTEND noninteractive

# Expose shiboken to apt-get
RUN echo deb http://us.archive.ubuntu.com/ubuntu xenial main universe >> /etc/apt/sources.list

RUN apt-get update && \
apt-get install -y \
software-properties-common && \
add-apt-repository -y ppa:thopiekar/pyside-git && \
apt-get update && apt-get install -y \
nano \
python3 \
python3-dev \
python3-pip \
Expand All @@ -16,6 +20,7 @@ RUN apt-get update && \
python3-pyside \
python3-pyside2 \
pyside2-tools \
libshiboken2-dev \
xvfb

# Nose is the Python test-runner
Expand Down
92 changes: 92 additions & 0 deletions Qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,50 @@ def _setup(module, extras):
setattr(Qt, name, _new_module(name))


def _wrapinstance(func, ptr, base=None):
"""Enable implicit cast of pointer to most suitable class

This behaviour is available in sip per default.

Based on http://nathanhorne.com/pyqtpyside-wrap-instance

Usage:
This mechanism kicks in under these circumstances.
1. Qt.py is using PySide 1 or 2.
2. A `base` argument is not provided.

See :func:`QtCompat.wrapInstance()`

Arguments:
func (function): Original function
ptr (long): Pointer to QObject in memory
base (QObject, optional): Base class to wrap with. Defaults to QObject,
which should handle anything.

Copy link
Collaborator

@fredrikaverpil fredrikaverpil May 30, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps we should add a "Returns:" to the docstring to make it easier to understand the different types of return values are possible here, as I'm guessing it's not always a QObject?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, and long must also change.

Not sure how to tackle the fact that long is absent from Python 3. I had a look at us internally taking int for both Python 2 and 3, but then we stray from the original PySide2.wrapInstance argument signature.

"""

assert isinstance(ptr, long), "Argument 'ptr' must be of type <long>"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I know, in Python3 there is no longer a long, and instead it should be an int in python3.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, you're right. This wasn't caught by Travis, I'll take a closer look as to why that is tomorrow.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, this has been fixed.

Turns out shiboken isn't supported by Python 3.5.
pyside/shiboken-setup#3

assert (base is None) or issubclass(base, Qt.QtCore.QObject), (
"Argument 'base' must be of type <QObject>")

if base is None:
q_object = func(long(ptr), Qt.QtCore.QObject)
meta_object = q_object.metaObject()
class_name = meta_object.className()
super_class_name = meta_object.superClass().className()

if hasattr(Qt.QtWidgets, class_name):
base = getattr(Qt.QtWidgets, class_name)

elif hasattr(Qt.QtWidgets, super_class_name):
base = getattr(Qt.QtWidgets, super_class_name)

else:
base = Qt.QtCore.QObject
Copy link
Collaborator

@fredrikaverpil fredrikaverpil May 30, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity, in which scenario(s) will base be set to Qt.QtCore.QObject?
Would you mind adding comments in the if/else conditions to make this a bit more grokable?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Likely never, all objects you could get from this function are subclasses of QObject. It's set as default here as a fallback, as anything, even a QWidget or your own subclass of a subclass of QMainWindow could be interpreted as QObject. In those cases, you still gain access to various superclass properties like objectName() but not the more specialised methods like resize() etc.


return func(long(ptr), base)


def _pyside2():
"""Initialise PySide2

Expand All @@ -660,6 +704,18 @@ def _pyside2():

Qt.__binding_version__ = module.__version__

try:
import shiboken2
Qt.QtCompat.wrapInstance = (
lambda ptr, base=None: _wrapinstance(
shiboken2.wrapInstance, ptr, base)
)
Qt.QtCompat.getCppPointer = lambda object: \
shiboken2.getCppPointer(object)[0]

except ImportError:
pass # Optional

if hasattr(Qt, "_QtUiTools"):
Qt.QtCompat.loadUi = lambda fname: \
Qt._QtUiTools.QUiLoader().load(fname)
Expand Down Expand Up @@ -694,6 +750,18 @@ def _pyside():

Qt.__binding_version__ = module.__version__

try:
import shiboken
Qt.QtCompat.wrapInstance = (
lambda ptr, base=None: _wrapinstance(
shiboken.wrapInstance, ptr, base)
)
Qt.QtCompat.getCppPointer = lambda object: \
shiboken.getCppPointer(object)[0]

except ImportError:
pass # Optional

if hasattr(Qt, "_QtUiTools"):
Qt.QtCompat.loadUi = lambda fname: \
Qt._QtUiTools.QUiLoader().load(fname)
Expand Down Expand Up @@ -738,6 +806,18 @@ def _pyqt5():
import PyQt5 as module
_setup(module, ["uic"])

try:
import sip
Qt.QtCompat.wrapInstance = (
lambda ptr, base=None: _wrapinstance(
sip.wrapinstance, ptr, base)
)
Qt.QtCompat.getCppPointer = lambda object: \
sip.unwrapinstance(object)

except ImportError:
pass # Optional

if hasattr(Qt, "_uic"):
Qt.QtCompat.loadUi = lambda fname: Qt._uic.loadUi(fname)

Expand Down Expand Up @@ -803,6 +883,18 @@ def _pyqt4():
import PyQt4 as module
_setup(module, ["uic"])

try:
import sip
Qt.QtCompat.wrapInstance = (
lambda ptr, base=None: _wrapinstance(
sip.wrapinstance, ptr, base)
)
Qt.QtCompat.getCppPointer = lambda object: \
sip.unwrapinstance(object)

except ImportError:
pass # Optional

if hasattr(Qt, "_uic"):
Qt.QtCompat.loadUi = lambda fname: Qt._uic.loadUi(fname)

Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,11 @@ Qt.py also provides compatibility wrappers for critical functionality that diffe

| Attribute | Returns | Description
|:------------------------|:------------|:------------
| `load_ui(fname=str)` | `QObject` | Minimal wrapper of PyQt4.loadUi and PySide equivalent
| `loadUi(fname=str)` | `QObject` | Minimal wrapper of PyQt4.loadUi and PySide equivalent
| `translate(...)` | `function` | Compatibility wrapper around [QCoreApplication.translate][]
| `setSectionResizeMode()`| `method` | Compatibility wrapper around [QAbstractItemView.setSectionResizeMode][]
| `wrapInstance(addr=long, type=QObject)` | `QObject` | Wrapper around `shiboken2.wrapInstance` and PyQt equivalent
| `getCppPointer(object=QObject)` | `long` | Wrapper around `shiboken2.getCppPointer` and PyQt equivalent

[QCoreApplication.translate]: https://doc.qt.io/qt-5/qcoreapplication.html#translate
[QAbstractItemView.setSectionResizeMode]: https://doc.qt.io/qt-5/qheaderview.html#setSectionResizeMode
Expand Down
47 changes: 47 additions & 0 deletions tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,53 @@ def test_cli():
assert out.startswith(b"usage: Qt.py"), "\n%s" % out


if PYTHON == 2:
def test_wrapInstance():
""".wrapInstance and .getCppPointer is identical across all bindings"""
from Qt import QtCompat, QtWidgets

app = QtWidgets.QApplication(sys.argv)

try:
button = QtWidgets.QPushButton("Hello world")
button.setObjectName("MySpecialButton")
pointer = QtCompat.getCppPointer(button)
widget = QtCompat.wrapInstance(long(pointer),
QtWidgets.QWidget)
assert isinstance(widget, QtWidgets.QWidget), widget
assert widget.objectName() == button.objectName()

# IMPORTANT: this differs across sip and shiboken.
if binding("PySide") or binding("PySide2"):
assert widget != button
else:
assert widget == button

finally:
app.exit()

def test_implicit_wrapInstance():
""".wrapInstance doesn't need the `base` argument"""
from Qt import QtCompat, QtWidgets

app = QtWidgets.QApplication(sys.argv)

try:
button = QtWidgets.QPushButton("Hello world")
button.setObjectName("MySpecialButton")
pointer = QtCompat.getCppPointer(button)
widget = QtCompat.wrapInstance(long(pointer))
assert isinstance(widget, QtWidgets.QWidget), widget
assert widget.objectName() == button.objectName()

if binding("PySide") or binding("PySide2"):
assert widget != button
else:
assert widget == button

finally:
app.exit()

if binding("PyQt4"):
def test_preferred_pyqt4():
"""QT_PREFERRED_BINDING = PyQt4 properly forces the binding"""
Expand Down