Why do we have a __feature__?#
History#
In PySide user story PYSIDE-1019, we tested certain ways to
make PySide more pythonic. The first idea was to support some
way to allow for snake_case
function names.
This feature is possible with relatively low compatibility problems, because having the same function with different names would be not so nice, but a possible low-effort solution.
When going to true_property
, things become different. When we
support properties as first class objects instead of getter
and setter functions, we get a conflict, because a function
cannot act as a property (no braces) and be a function at the
same time.
This consideration led us to the idea: Features must be selectable per-module.
Why are features selectable per-module?#
Suppose you have some pre-existing code. Maybe you use some downloaded code or you generated an interface file. When you now decide to use a feature, you don’t want all this existing stuff to become incorrect. By using the statement
from __feature__ import ...
you declare that this module uses some feature. Other modules will not be influenced by this decision and can stay unchanged.
Why dunder, and why not __future__?#
Especially in Python 2, but in a few cases also in Python 3, there is the future statement
from __future__ import ...
That is a statement that can only appear at the beginning of a module, and it switches how the Python parser works.
Our first idea was to mimick this behavior for PySide, although we are a bit cheating: The feature statement is not a syntactical construct, and we cannot easily forbid that it is in the middle of a module.
We then realized that the intention of Python’s __future__
import and
PySide’s __feature__
import are different: While Python implies by
__future__
some improvement, we do not want to associate with
__feature__
. We simply think that some users who come from Python may
like our features, while others are used to the C++ convention and
consider something that deviates from the Qt documentation as drawback.
The intention to use the from __feature__ import ...
notation was the hope that
people see the similarity to Python’s __future__
statement and put that import
at the beginning of a module to make it very visible that this module
has some special global differences.
The snake_case feature#
By using the statement
from __feature__ import snake_case
all methods of all classes used in this module are changing their name.
The algorithm to change names is this:
if the name has less than 3 chars, or
if two upper chars are adjacent, or
if the name starts with
gl
(which marks OpenGL),the name is returned unchanged. Otherwise
a single upper char
C
is replaced by_c
The true_property feature#
By using the statement
from __feature__ import true_property
all methods of all classes used in this module which are declared in the Qt documentation as property become real properties in Python.
This feature is incompatible with the past and cannot coexist; it is the reason why the feature idea was developed at all.
Normal Properties#
Normal properties have the same name as before:
QtWidgets.QLabel().color()
becomes as property
QtWidgets.QLabel().color
When there is also a setter method,
QtWidgets.QLabel().setColor(value)
becomes as property
QtWidgets.QLabel().color = value
Normal properties swallow the getter and setter functions and replace them by the property object.
Special Properties#
Special properties are those with non-standard names.
QtWidgets.QLabel().size()
becomes as property
QtWidgets.QLabel().size
But here we have no setSize function, but
QtWidgets.QLabel().resize(value)
which becomes as property
QtWidgets.QLabel().size = value
In that case, the setter does not become swallowed, because so many
people are used to the resize
function.
Class properties#
It should be mentioned that we not only support regular properties as they are known from Python. There is also the concept of class properties which always call their getter and setter:
A regular property like the aforementioned QtWidgets.QLabel
has
this visibility:
>>> QtWidgets.QLabel.size
<property object at 0x113a23540>
>>> QtWidgets.QLabel().size
PySide6.QtCore.QSize(640, 480)
A class property instead is also evaluated without requiring an instance:
>>> QtWidgets.QApplication.windowIcon
<PySide6.QtGui.QIcon(null) at 0x113a211c0>
You can only inspect it if you go directly to the right class dict:
>>> QtGui.QGuiApplication.__dict__["windowIcon"]
<PySide6.PyClassProperty object at 0x114fc5270>
About Property Completeness#
There are many properties where the Python programmer agrees that these functions should be properties, but a few are not properties, like
>>> QtWidgets.QMainWindow.centralWidget
<method 'centralWidget' of 'PySide6.QtWidgets.QMainWindow' objects>
We are currently discussing if we should correct these rare cases, as they are probably only omissions. Having to memorize the missing properties seems to be quite cumbersome, and instead of looking all properties up in the Qt documentation, it would be easier to add all properties that should be properties and are obviously missing.
Name Clashes and Solution#
There are some rare cases where a property already exists as a function, either with multiple signatures or having parameters. This is not very nice in C++ as well, but for Python this is forbidden. Example:
>>> from PySide6 import *
>>> from PySide6.support.signature import get_signature
>>> import pprint
>>> pprint.pprint(get_signature(QtCore.QTimer.singleShot))
[<Signature (arg__1: int, arg__2: Callable) -> None>,
<Signature (msec: int, receiver: PySide6.QtCore.QObject, member: bytes) -> None>,
<Signature (msec: int, timerType: PySide6.QtCore.Qt.TimerType,
receiver: PySide6.QtCore.QObject, member: bytes) -> None>]
When creating this property, we respect the existing function and use a slightly different name for the property by appending an underscore.
>>> from __feature__ import true_property
>>> QtCore.QTimer.singleShot_
<property object at 0x118e5f8b0>
We hope that these clashes can be removed in future Qt versions.
The __feature__ import#
The implementation of from __feature__ import ...
is built by a slight
modification of the __import__
builtin. We made that explicit by assigning
variables in the builtin module. This modification takes place at Qt for Python
import time:
The original function in
__import__
is kept in__orig_import__
.The new function is in
__feature_import__
and assigned to__import__
.
This function calls the Python function PySide6.support.__feature__.feature_import
first, and falls back to __orig_import__
if feature import is not applicable.
Overriding __import__#
This is not recommended. Import modifications should be done using import hooks, see the Python documentation on Import-Hooks.
If you would like to modify __import__
anyway without destroying the features,
please override just the __orig_import__
function.
IDEs and Modifying Python stub files#
Qt for Python comes with pre-generated .pyi
stub files in the same location as
the binary module. For instance, in the site-packages directory, you can find
a QtCore.pyi
file next to QtCore.abi3.so
or QtCore.pyd
on Windows.
When using __feature__
often with common IDEs, you may want to provide
a feature-aware version of .pyi
files to get a correct display. The simplest
way to change them all in-place is the command:
pyside6-genpyi all --feature snake_case true_property
Using __feature__ with UIC files#
Features can be freely used together with generated UIC files. The UIC files are _not_ converted, intentionally. Mixing them with feature selections in other Python modules should always work, because switching will happen as needed, selected by the currently active module. (Please report to us if this fails for an example)