2

I believe it is common practice (at least it is for me) to be able to wrap function calls.

For example, as an example, a minimilistic wrapping function looks like:

def wrap(fn, *args, **kwargs):
    return fn(*args, **kwargs)

And you could call an arbitrary method via

wrap(qt_method, 1, 2, foo='bar')

which would be equivalent to directly calling

qt_method(1,2, foo='bar')

This generally works for me. However, I've come across a case where it doesn't.

QWebView.load() doesn't seem to like having an empty dictionary expanded into its call signature. E.g. wrap(my_webview.load, QUrl('http://istonyabbottstillprimeminister.com')) fails with the exception: TypeError: QWebView.load(QUrl): argument 1 has unexpected type 'QUrl'.

Below is a minimilistic working example that demonstrates things that work, and don't. I have yet to find another Qt method that fails to wrap like this.

import sys
from PyQt4 import QtCore
from PyQt4 import QtGui
from PyQt4.QtWebKit import QWebView

qapplication = QtGui.QApplication(sys.argv)

webview = QWebView()

url = QtCore.QUrl('http://istonyabbottstillprimeminister.com')

# basic wrapping function
def wraps(fn, *args, **kwargs):
    return fn(*args, **kwargs)

args = ()
kwargs = {}

wraps(webview.load, url)     # Doesn't work
webview.load(url, **kwargs)  # Doesn't work
webview.load(url)            # works
webview.url(*args, **kwargs) # works

webview.show()
qapplication.exec_()

This problem also applies when you subclass QWebView and override the load method like this:

def load(self *args, **kwargs):
    return QWebView.load(self, *args, **kwargs)

Of course, if you instead call QWebView.load(self, *args) or do not use *args, **kwargs in the methods signature, then you don't get the exception (which follows what is seen from the minimilistic working example ebove)

Any insight into this would be appreciated.

three_pineapples
  • 11,579
  • 5
  • 38
  • 75
  • I think you should edit your question to focus on the core issue. The fact that `webview.load(url, **{})` does not work shows that this doesn't really have anything to do with "wrapping". It's just that `QWebView.load` doesn't work with keyword arguments. – BrenBarn Feb 09 '15 at 03:24
  • I only partially agree with you. There is no reason why anyone would ever want to unpack an **empty** dictionary into keyword arguments when directly calling a function. The only legitimate case where this happens is when you are creating a generic function wrapper or overriding a method when subclassing. As such, I felt any attempt to remove the problem from the use case would have resulted in people simply telling me never to call `webview.load` as `webview.load(url, **{})` – three_pineapples Feb 09 '15 at 07:37

3 Answers3

4

I think this is an artifact of PyQt being a Python wrapper for a C++ library. Note that QWebView.load is not a function written in Python, but a "builtin method" provided by a compiled extension. The documentation for QWebView.load shows two versions with different argument signatures. This sort of method overloading is not possible in Python, so presumably QWebView.load is using some heuristics to determine which version you want to call, and these heuristics are failing when you use kwargs. (A Python function can't tell the difference between not passing kwargs at all and passing empty kwargs, but a C extension module can.) In other words, when you call load(url, **kwargs), it thinks you're trying to use the version of load that accepts a QNetworkRequest rather than a QUrl.

Unfortunately this sort of problem sometimes arises when using Python wrappers for C++ libraries, because C++ APIs may make use of overloaded function signatures, which have to somehow be translated into the Python world where overloading is not possible. It might be worth raising an issue on the PyQt4 bug tracker. Presumably this could be fixed by making the underlying heuristic smarter (for instance, it should treat empty kwargs the same as omitted kwargs). In the meantime, you will have to work around it by explicitly checking if you're about to call QWebView.load, and not passing kwargs if so.

Note that the issue doesn't have anything to do with function-wrapping as you describe it in your program. webview.load(url, **{}) fails on its own, without writing any wrappers at all. This means the problem is in the way QWebView.load handles keyword arguments. You found this because you wrote a wrapper around it, but the problem exists with or without the wrapper.

BrenBarn
  • 242,874
  • 37
  • 412
  • 384
  • 1
    Unless I'm missing something, doesn't the fact that the exception says `TypeError: QWebView.load(QUrl)` indicate it has found the correct version of `load`? – three_pineapples Feb 09 '15 at 07:39
  • @three_pineapples: I don't think so. The docstring for `QWebView.load` shows both argument signatures, so I think it's just that the QUrl version is the "default" one (or at least the default name/docstring) and it fails during its attempt to dispatch from there. – BrenBarn Feb 09 '15 at 17:18
  • @BrenBarn. I think three-pineapples has a point, because PyQt will usually show *all* the availlable overloads when there's a TypeError due to invalid/ambiguous arguments. This issue really needs to be reported on the [PyQt mailing list](http://www.riverbankcomputing.com/mailman/listinfo/pyqt), though - there's little to be gained from discussing it further here. – ekhumoro Feb 09 '15 at 18:33
4

I'm unable to reproduce this except with overloaded PyQt methods that have a specific combination of call signatures. Only when one signature has a single argument, and another has a single argument and the rest keyword arguments with default values, does this bug seem to be hit.

There are exactly nine such methods in PyQt4:

QtNetwork
  QSslSocket
    addDefaultCaCertificates(QString, QSsl.EncodingFormat format=QSsl.Pem, QRegExp.PatternSyntax syntax=QRegExp.FixedString)
    addDefaultCaCertificates(list-of-QSslCertificate)

    addCaCertificates(QString, QSsl.EncodingFormat format=QSsl.Pem, QRegExp.PatternSyntax syntax=QRegExp.FixedString)
    addCaCertificates(list-of-QSslCertificate)

    setPrivateKey(QSslKey)
    setPrivateKey(QString, QSsl.KeyAlgorithm algorithm=QSsl.Rsa, QSsl.EncodingFormat format=QSsl.Pem, QByteArray passPhrase=QByteArray())

    setLocalCertificate(QSslCertificate)
    setLocalCertificate(QString, QSsl.EncodingFormat format=QSsl.Pem)

QtWebKit
  QWebFrame
    load(QUrl)
    load(QNetworkRequest, QNetworkAccessManager.Operation operation=QNetworkAccessManager.GetOperation, QByteArray body=QByteArray())

  QGraphicsWebView
    load(QUrl)
    load(QNetworkRequest, QNetworkAccessManager.Operation operation=QNetworkAccessManager.GetOperation, QByteArray body=QByteArray())

  QWebView
    load(QUrl)
    load(QNetworkRequest, QNetworkAccessManager.Operation operation=QNetworkAccessManager.GetOperation, QByteArray body=QByteArray())

QtGui
  QGraphicsScene
    items(Qt.SortOrder)
    QGraphicsScene.items(QPointF)
    QGraphicsScene.items(QRectF, Qt.ItemSelectionMode mode=Qt.IntersectsItemShape)
    QGraphicsScene.items(QPolygonF, Qt.ItemSelectionMode mode=Qt.IntersectsItemShape)
    QGraphicsScene.items(QPainterPath, Qt.ItemSelectionMode mode=Qt.IntersectsItemShape)

  QGraphicsView
    items(QPoint)
    QGraphicsView.items(QRect, Qt.ItemSelectionMode mode=Qt.IntersectsItemShape)
    QGraphicsView.items(QPolygon, Qt.ItemSelectionMode mode=Qt.IntersectsItemShape)
    QGraphicsView.items(QPainterPath, Qt.ItemSelectionMode mode=Qt.IntersectsItemShape)

and you just happened to hit on one. I would report this as a bug against SIP. Its heuristics for figuring out which method to call seem in general very good, and it happens to be failing just for this particular combination or call signatures. If you want to maximise confidence that things won't break when fed arbitrary functions that might be parsing their arguments poorly, I'd use something like this your code:

def call_method_considerately(method, *args, **kwargs):
    if args and kwargs:
        return method(*args, **kwargs)
    elif args:
        return method(*args)
    elif kwargs:
        return method(**kwargs)
    else:
        return method()
three_pineapples
  • 11,579
  • 5
  • 38
  • 75
Chris Billington
  • 855
  • 1
  • 8
  • 14
1

As python doesn't support function overloaded, so the signature of python function usually contains *args or **kwargs for simulating "overloaded". C++ Qt implements a lot of overloaded functions, like here: http://qt-project.org/doc/qt-4.8/qwebview.html they have:

void    load ( const QUrl & url )
void    load ( const QNetworkRequest & request, QNetworkAccessManager::Operation operation = QNetworkAccessManager::GetOperation, const QByteArray & body = QByteArray() )
implements the overloaded functions.

However, if you look into the PyQt4 function signature( the function prototype, they doesn't always have the **kwargs) , take the load as example, the load defines in PyQt4 is actually:

 def load(self, *__args): # real signature unknown; restored from __doc__ with multiple overloads
    """
    QWebView.load(QUrl)
    QWebView.load(QNetworkRequest, QNetworkAccessManager.Operation operation=QNetworkAccessManager.GetOperation, QByteArray body=QByteArray())
    """
    pass

so you might try this:

qapplication = QtGui.QApplication(sys.argv)

webview = QWebView()

url = QtCore.QUrl('http://istonyabbottstillprimeminister.com')


# basic wrapping function
def wraps(fn, *args, **kwargs):
    if not len(kwargs):
        return fn(*args)
    else:
        return fn(*args, **kwargs)

args = ()
kwargs = {}

wraps(webview.load, url)
webview.load(url)

webview.show()
qapplication.exec_()

So, in this way, you can use the doc to detect the possible function signature.

print webview.load.__doc__

Output is QWebView.load(QUrl) QWebView.load(QNetworkRequest, QNetworkAccessManager.Operation operation=QNetworkAccessManager.GetOperation, QByteArray body=QByteArray())

Cui Heng
  • 1,265
  • 8
  • 10