"""
Modify enums in SIP 6 to behave like SIP 4 (e.g. allow
`QComboBox.InsertAlphabetically` in addition to
`QComboBox.InsertPolicy.InsertAlphabetically`). Note that the functions in this
module are no-ops under Qt 5.
The typical use for this module would be to compile a SIP module to
`schrodinger.whatever._my_sip_module` and then create a my_sip_module.py that
contains::
from ._my_sip_module import * # noqa F403
from schrodinger.infra import fix_sip6_enum_access
fix_sip6_enum_access.modify_enum_access(globals(), __name__)
del fix_sip6_enum_access
"""
import enum
try:
from PyQt6 import sip
except ImportError:
is_qt6 = False
else:
is_qt6 = True
# The StyleOptionType and StyleOptionVersion enums are intentionally redefined
# in each QStyleHintReturn or QStyleOption subclass, so we expect naming
# clashes. In the code below, we allow each subclass's enum to override the
# inherited enum, which mimics the C++ behavior.
_KNOWN_CLASHES = (
("StyleOptionType", "Type"),
("StyleOptionVersion", "Version"),
)
_UNKNOWN_CLASH_ERR = "Naming clash for enum {}.{} in {}"
# Some enum are declared using "enum class" instead of "enum". Access to these
# enum classes is scoped in C++ (and in PyQt 5, PySide, etc.) and allowing for
# unscoped access can lead to name clashes. For example, QColorSpace.Primaries
# and QColorSpace.TransferFunction both have an enum member named "SRgb", so
# QColorSpace.SRgb would be ambiguous. Similarly,
# QtNetwork.QNetworkInformation.Feature has an enum member named "Reachability"
# even though there is a separate enum class named
# QtNetwork.QNetworkInformation.Reachability. There's no way to distinguish
# enum classes from regular enums through introspection in PyQt, so we hard code
# a list of all enum classes. This list was built by searching through the Qt
# header files and our *.sip files, e.g.:
# grep -E "\benum\s+class\b" /software/lib/Linux-x86_64/qt-6.2.1/include/QtCore/*
_ENUM_CLASSES = (
# QtCharts
("QtCharts.QXYSeries", "PointConfiguration"),
# QtCore
("QtCore.QAbstractItemModel", "CheckIndexOption"),
("QtCore.QByteArray", "Base64DecodingStatus"),
("QtCore.QCalendar", "System"),
("QtCore.QDateTime", "YearRange"),
("QtCore.QStringConverter", "Flag"),
# QtGui
("QtGui.QActionGroup", "ExclusionPolicy"),
("QtGui.QColorSpace", "Primaries"),
("QtGui.QColorSpace", "TransferFunction"),
("QtGui.QInputDevice", "DeviceType"),
("QtGui.QInputDevice", "Capability"),
("QtGui.QTextFormat", "MarkerType"),
# QtNetwork
("QtNetwork.QNetworkCookie", "SameSite"),
("QtNetwork.QOcspResponse", "QOcspCertificateStatus"),
("QtNetwork.QOcspResponse", "QOcspRevocationReason"),
("QtNetwork.QSslCertificate", "PatternSyntax"),
("QtNetwork.QNetworkInformation", "Feature"),
("QtNetwork.QNetworkInformation", "Reachability"),
("QtNetwork.QSsl", "AlertLevel"),
("QtNetwork.QSsl", "AlertType"),
("QtNetwork.QSsl", "ImplementedClass"),
("QtNetwork.QSsl", "SupportedFeature"),
# schrodinger
("schrodinger.infra.jobhub", "JobOption"),
("schrodinger.infra.jobhub", "JobStatusNotif"),
("schrodinger.ui.maestro_ui", "AppProfileMode"),
("schrodinger.ui.maestro_ui", "InteractionMode"),
("schrodinger.ui.maestro_ui", "LigandReceptorInteractions"),
("schrodinger.ui.maestro_ui.maestro", "MoveType"),
("schrodinger.ui.sketcher", "COORDINATE_GENERATION"),
("schrodinger.ui.sketcher", "ColorHeteroatomsMode"),
)
def modify_enum_access(namespace_dict: dict, namespace_name: str):
"""
Iterate through the given namespace dictionary and modify the namespace
itself as well as all classes and nested namespaces it contains so that
enums can be accessed directly through a class/namespace without going
through the enum class (e.g. allow for `QComboBox.InsertAlphabetically` or
`Qt.AlignLeft` in addition to `QComboBox.InsertPolicy.InsertAlphabetically`
or `Qt.Alignment.AlignLeft`).
:param namespace_dict: The module namespace dictionary to modify. To modify
a module, pass in e.g. `QtCore.__dict__`. To modify a module from
within that module, pass in `globals()`.
:param namespace_name: The name of the namespace being modified. If this is
a Qt namespace, omit the leading "PyQt6." or "schrodinger.Qt.", e.g. use
"QtCore" instead of "PyQt6.QtCore".
"""
for attr_name, attr in namespace_dict.copy().items():
if (namespace_name, attr_name) in _ENUM_CLASSES:
continue
elif isinstance(attr, enum.EnumMeta):
for enum_val in attr:
if (enum_val.name in namespace_dict and
not (attr_name, enum_val.name) in _KNOWN_CLASHES):
raise RuntimeError(
_UNKNOWN_CLASH_ERR.format(attr_name, enum_val,
namespace_name))
namespace_dict[enum_val.name] = enum_val
if isinstance(attr, sip.wrappertype):
# We can't recurse on modify_enum_access here because Class.__dict__
# is a read-only mappingproxy (unlike module.__dict__, which is an
# actual dict instance). Because of this, passing a class
# dictionary into modify_enum_access would lead to "TypeError:
# 'mappingproxy' object does not support item assignment"
_modify_enum_access_nested(attr, f"{namespace_name}.{attr_name}")
def _modify_enum_access_nested(namespace: type, namespace_name: str):
"""
Equivalent to `modify_enum_access`, but takes a namespace instead of a
namespace dictionary.
:param namespace: The class or namespace to modify.
:param namespace_name: The name of the namespace being modified.
"""
for attr_name, attr in namespace.__dict__.copy().items():
if (namespace_name, attr_name) in _ENUM_CLASSES:
continue
elif isinstance(attr, enum.EnumMeta):
for enum_val in attr:
if (hasattr(namespace, enum_val.name) and
not (attr_name, enum_val.name) in _KNOWN_CLASHES):
raise RuntimeError(
_UNKNOWN_CLASH_ERR.format(attr_name, enum_val,
namespace_name))
setattr(namespace, enum_val.name, enum_val)
elif isinstance(attr, sip.wrappertype):
_modify_enum_access_nested(attr, f"{namespace_name}.{attr_name}")
# turn the above methods into no-ops if we're in a SIP 4 build
if not is_qt6:
[docs] def modify_enum_access(*args, **kwargs): # noqa: F811
pass
def _modify_enum_access_nested(*args, **kwargs): # noqa: F811
pass