Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
29 changes: 21 additions & 8 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Qt.py was born to address the growing needs in these industries for the developm
- [Development goals](#development-goals)
- [Support co-existence](#support-co-existence)
- [Keep it simple](#keep-it-simple)
- [No bugs](#no-bugs)
- [No wrappers](#no-wrappers)
- [How can I contribute?](#how-can-i-contribute)
- [Reporting Bugs](#reporting-bugs)
- [Suggesting Enhancements](#suggesting-enhancements)
Expand All @@ -28,11 +28,11 @@ Qt.py was born to address the growing needs in these industries for the developm

Qt.py was born in the film and visual effects industry to address the growing needs for the development of software capable of running with more than one flavor of the Qt bindings for Python - PySide, PySide2, PyQt4 and PyQt5.

| Goal | Description
|:--------------------------|:---------------
| *Support co-existence* | Qt.py should not affect other bindings running in same interpreter session.
| *Keep it simple* | One file, copy/paste installation, PEP08.
| *No bugs* | No implementations = No bugs.
| Goal | Description
|:---------------------------|:---------------
| [*Support co-existence*](#support-coexistence) | Qt.py should not affect other bindings running in same interpreter session.
| [*Keep it simple*](#keep-it-simple) | One file, copy/paste installation, PEP08.
| [*No wrappers*](#no-wrappers) | Don't attempt to fill in for missing functionality in a binding.

Each of these deserve some explanation and rationale.

Expand All @@ -55,8 +55,7 @@ QtWidgets.QApplication.translate = staticmethod(translate)

```python
# Right
...
QtWidgets.QApplication.translate_ = staticmethod(translate)
QtCompat.translate = translate
```

<br>
Expand All @@ -67,6 +66,20 @@ At the end of the day, Qt.py is a middle-man. It delegates requests you make to

<br>

#### No wrappers

One approach at bridging two different implementations is by implementing missing functionality yourself.

A [common example](https://gist.github.com/cpbotha/1b42a20c8f3eb9bb7cb8) of this is the differing argument signature in `loadUi` from PyQt4 versus PySide.

One problem with this approach is that bindings are already wrapping an original implementation and carries a large surface area for bugs with it. By wrapping it once more, we multiply this surface area, resulting in potential for even more obscure bugs that may take years to experience and filter out.

By instead limiting the argument signature to ones they both share, we both (1) reduce the surface area (2) avoid introducing additional bugs.

We believe neither approach is right or wrong - this is simply the approach taken here that turns out to be the easier and more robust rule to follow consistently as a team.

<br>

##### No bugs

This may seem like an impossible requirement, but hear me out. Bugs stem from implementations. Therefore, if there are no implementations, there can be no bugs.
Expand Down
121 changes: 75 additions & 46 deletions Qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,32 @@
import sys
import shutil

self = sys.modules[__name__]
# Flags from environment variables
QT_VERBOSE = bool(os.getenv("QT_VERBOSE")) # Extra output
QT_TESTING = bool(os.getenv("QT_TESTING")) # Extra constraints
QT_PREFERRED_BINDING = os.getenv("QT_PREFERRED_BINDING") # Override default

self.__version__ = "0.6.1"
self = sys.modules[__name__]

self.__added__ = list() # All unique members of Qt.py
# Internal members, may be used externally for debugging
self.__added__ = list() # All members added to QtCompat
self.__remapped__ = list() # Members copied from elsewhere
self.__modified__ = list() # Existing members modified in some way

# Below members are set dynamically on import relative the original binding.
self.__version__ = "0.6.2"
self.__qt_version__ = "0.0.0"
self.__binding__ = "None"
self.__binding_version__ = "0.0.0"

self.load_ui = lambda fname: None
self.translate = lambda context, sourceText, disambiguation, n: None
self.setSectionResizeMode = lambda *args, **kwargs: None
self.setSectionResizeMode = lambda logicalIndex, hide: None

# All members of this module is directly accessible via QtCompat
# Take care not to access any "private" members; i.e. those with
# a leading underscore.
QtCompat = self


def convert(lines):
Expand Down Expand Up @@ -89,7 +100,7 @@ def _remap(object, name, value, safe=True):

"""

if os.getenv("QT_TESTING") is not None and safe:
if QT_TESTING is not None and safe:
# Cannot alter original binding.
if hasattr(object, name):
raise AttributeError("Cannot override existing name: "
Expand All @@ -112,7 +123,7 @@ def _remap(object, name, value, safe=True):
def _add(object, name, value):
"""Append to self, accessible via Qt.QtCompat"""
self.__added__.append(name)
setattr(self, name, value)
setattr(object, name, value)


def _pyqt5():
Expand All @@ -123,13 +134,10 @@ def _pyqt5():
_remap(QtCore, "Slot", QtCore.pyqtSlot)
_remap(QtCore, "Property", QtCore.pyqtProperty)

_add(PyQt5, "__binding__", PyQt5.__name__)
_add(PyQt5, "load_ui", lambda fname: uic.loadUi(fname))
_add(PyQt5, "translate", lambda context, sourceText, disambiguation, n: (
QtCore.QCoreApplication(context, sourceText,
disambiguation, n)))
_add(PyQt5,
"setSectionResizeMode",
_add(QtCompat, "__binding__", PyQt5.__name__)
_add(QtCompat, "load_ui", lambda fname: uic.loadUi(fname))
_add(QtCompat, "translate", QtCore.QCoreApplication.translate)
_add(QtCompat, "setSectionResizeMode",
QtWidgets.QHeaderView.setSectionResizeMode)

_maintain_backwards_compatibility(PyQt5)
Expand Down Expand Up @@ -172,16 +180,27 @@ def _pyqt4():
from PyQt4 import QtWebKit
_remap(PyQt4, "QtWebKitWidgets", QtWebKit)
except ImportError:
# QtWebkit is optional in Qt , therefore might not be available
pass

_add(PyQt4, "QtCompat", self)
_add(PyQt4, "__binding__", PyQt4.__name__)
_add(PyQt4, "load_ui", lambda fname: uic.loadUi(fname))
_add(PyQt4, "translate", lambda context, sourceText, disambiguation, n: (
QtCore.QCoreApplication(context, sourceText,
disambiguation, None, n)))
_add(PyQt4, "setSectionResizeMode", QtGui.QHeaderView.setResizeMode)
"QtWebkit is optional in Qt , therefore might not be available"

_add(QtCompat, "__binding__", PyQt4.__name__)
_add(QtCompat, "load_ui", lambda fname: uic.loadUi(fname))

# The second argument - hide - does not apply to Qt4
_add(QtCompat, "setSectionResizeMode",
lambda logicalIndex, hide:
QtGui.QHeaderView.setResizeMode(logicalIndex))

# PySide2 differs from Qt4 in that Qt4 has one extra argument
# which is always `None`. The lambda arguments represents the PySide2
# interface, whereas the arguments passed to `.translate` represent
# those expected of a Qt4 binding.
_add(QtCompat, "translate",
lambda context, sourceText, disambiguation, n:
QtCore.QCoreApplication.translate(context,
sourceText,
disambiguation,
None,
n))

_maintain_backwards_compatibility(PyQt4)

Expand All @@ -194,15 +213,20 @@ def _pyside2():

_remap(QtCore, "QStringListModel", QtGui.QStringListModel)

_add(PySide2, "__binding__", PySide2.__name__)
_add(PySide2, "load_ui", lambda fname: QtUiTools.QUiLoader().load(fname))
_add(PySide2, "translate", lambda context, sourceText, disambiguation, n: (
QtCore.QCoreApplication(context, sourceText,
disambiguation, None, n)))
_add(PySide2,
"setSectionResizeMode",
_add(QtCompat, "__binding__", PySide2.__name__)
_add(QtCompat, "load_ui", lambda fname: QtUiTools.QUiLoader().load(fname))

_add(QtCompat, "setSectionResizeMode",
QtWidgets.QHeaderView.setSectionResizeMode)

_add(QtCompat, "translate",
lambda context, sourceText, disambiguation, n:
QtCore.QCoreApplication.translate(context,
sourceText,
disambiguation,
None,
n))

_maintain_backwards_compatibility(PySide2)

return PySide2
Expand All @@ -223,15 +247,22 @@ def _pyside():
from PySide import QtWebKit
_remap(PySide, "QtWebKitWidgets", QtWebKit)
except ImportError:
# QtWebkit is optional in Qt, therefore might not be available
pass
"QtWebkit is optional in Qt, therefore might not be available"

_add(QtCompat, "__binding__", PySide.__name__)
_add(QtCompat, "load_ui", lambda fname: QtUiTools.QUiLoader().load(fname))

_add(QtCompat, "setSectionResizeMode",
lambda logicalIndex, hide:
QtGui.QHeaderView.setResizeMode(logicalIndex))

_add(PySide, "__binding__", PySide.__name__)
_add(PySide, "load_ui", lambda fname: QtUiTools.QUiLoader().load(fname))
_add(PySide, "translate", lambda context, sourceText, disambiguation, n: (
QtCore.QCoreApplication(context, sourceText,
disambiguation, None, n)))
_add(PySide, "setSectionResizeMode", QtGui.QHeaderView.setResizeMode)
_add(QtCompat, "translate",
lambda context, sourceText, disambiguation, n:
QtCore.QCoreApplication.translate(context,
sourceText,
disambiguation,
None,
n))

_maintain_backwards_compatibility(PySide)

Expand Down Expand Up @@ -311,17 +342,15 @@ def init():

"""

preferred = os.getenv("QT_PREFERRED_BINDING")
verbose = os.getenv("QT_VERBOSE") is not None
bindings = (_pyside2, _pyqt5, _pyside, _pyqt4)

if preferred:
if QT_PREFERRED_BINDING:
# Internal flag (used in installer)
if preferred == "None":
if QT_PREFERRED_BINDING == "None":
self.__wrapper_version__ = self.__version__
return

preferred = preferred.split(os.pathsep)
preferred = QT_PREFERRED_BINDING.split(os.pathsep)
available = {
"PySide2": _pyside2,
"PyQt5": _pyqt5,
Expand All @@ -338,19 +367,19 @@ def init():
)

for binding in bindings:
_log("Trying %s" % binding.__name__, verbose)
_log("Trying %s" % binding.__name__, QT_VERBOSE)

try:
binding = binding()

except ImportError as e:
_log(" - ImportError(\"%s\")" % e, verbose)
_log(" - ImportError(\"%s\")" % e, QT_VERBOSE)
continue

else:
# Reference to this module
binding.__shim__ = self
binding.QtCompat = self
binding.__shim__ = self # DEPRECATED

sys.modules.update({
__name__: binding,
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,15 @@ Qt.py enables you to write software that dynamically chooses the most desireable

### Project goals

Write for PySide2, run in any binding.

Qt.py was born in the film and visual effects industry to address the growing need for the development of software capable of running with more than one flavour of the Qt bindings for Python - PySide, PySide2, PyQt4 and PyQt5.
Qt.py was born in the film and visual effects industry to address the growing need for software capable of running with more than one flavor of the Qt bindings for Python - PySide, PySide2, PyQt4 and PyQt5.

| Goal | Description
|:-------------------------------------|:---------------
| *Build for one, run with all* | You code written with Qt.py should run on any binding.
| *Explicit is better than implicit* | Differences between bindings should be visible to you.
| *Support co-existence* | Qt.py should not affect other bindings running in same interpreter session.
| *Build for one, run with all* | Code written with Qt.py should run on any binding.
| *Explicit is better than implicit* | Differences between bindings should be visible to you.

See [`CONTRIBUTING.md`](CONTRIBUTING.md) for more details.

Expand All @@ -45,7 +46,7 @@ See [`CONTRIBUTING.md`](CONTRIBUTING.md) for more details.

### Install

Qt.py is a single file and can either be [copy/pasted](https://raw.githubusercontent.com/mottosso/Qt.py/master/Qt.py) into your project, [downloaded](https://github.com/mottosso/Qt.py/archive/master.zip) as-is or installed via PyPI.
Qt.py is a single file and can either be [copy/pasted](https://raw.githubusercontent.com/mottosso/Qt.py/master/Qt.py) into your project, [downloaded](https://github.com/mottosso/Qt.py/archive/master.zip) as-is, cloned as-is or installed via PyPI.

```bash
$ pip install Qt.py
Expand Down