Created
April 5, 2023 04:56
-
-
Save RoadrunnerWMC/c7ba91a8f31bb1cd9b99b8926d27552c to your computer and use it in GitHub Desktop.
A proof-of-concept for a __repr__ function for PyQt QColor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
def QColor_repr(self) -> str: | |
""" | |
PyQt6.QtGui.QColor.__repr__ | |
""" | |
PREFIX = 'PyQt6.QtGui.QColor' | |
spec = self.spec() | |
if spec == QtGui.QColor.Spec.Invalid: | |
return f'{PREFIX}()' | |
rgba64 = self.rgba64() | |
a64 = rgba64.alpha() | |
if spec == QtGui.QColor.Spec.ExtendedRgb: | |
# ExtendedRgb uses 16-bit *floats* internally, so we can't | |
# really do much better than this: | |
if a64 == 65535: | |
return f'{PREFIX}.fromRgbF({self.redF()}, {self.greenF()}, {self.blueF()})' | |
else: | |
return f'{PREFIX}.fromRgbF({self.redF()}, {self.greenF()}, {self.blueF()}, {self.alphaF()})' | |
# Each spec type (other than the two above) has a "short form" and a | |
# "precise form" constructor. We'd *like* to display using the short | |
# form if possible, but we need to fall back to the precise form if | |
# this color's value is not exactly representable that way. | |
# | |
# Each of the below `if` blocks needs to create the following | |
# variables: | |
# | |
# - short_form_method_name: the display name of the short-form | |
# QColor constructor for this spec type | |
# - precise_form_method_name: the display name of the precise-form | |
# QColor constructor for this spec type | |
# - components: a list of the color components to use as arguments. | |
# Each one is a 3-tuple with the following elements: | |
# - precise_value: if we have to use the "precise form", this is | |
# the value we'll display | |
# - internal_value: the reconstructed 16-bit value stored in the | |
# QColor | |
# - internal_step: when the short-form constructor is used, each | |
# "+ 1" increment of this argument corresponds to an increase | |
# in the resulting internal value by this amount | |
# | |
# If all of the components have internal values that are multiples | |
# of their internal steps, then we can safely use the short-form | |
# constructor. Otherwise, we use the precise form. | |
if spec == QtGui.QColor.Spec.Rgb: | |
# This is the only one that's straightforward to get the | |
# internal value for. | |
short_form_method_name = '' # shorter than '.fromRgb' | |
precise_form_method_name = '.fromRgba64' | |
r = rgba64.red() | |
g = rgba64.green() | |
b = rgba64.blue() | |
components = [ | |
(r, r, 257), | |
(g, g, 257), | |
(b, b, 257), | |
(a64, a64, 257), | |
] | |
elif spec == QtGui.QColor.Spec.Hsv: | |
short_form_method_name = '.fromHsv' | |
precise_form_method_name = '.fromHsvF' | |
h = self.hsvHueF() | |
s = self.hsvSaturationF() | |
v = self.valueF() | |
components = [ | |
(h, round(h * 36000), 100), | |
(s, round(s * 65535), 257), | |
(v, round(v * 65535), 257), | |
(self.alphaF(), a64, 257), | |
] | |
elif spec == QtGui.QColor.Spec.Cmyk: | |
short_form_method_name = '.fromCmyk' | |
precise_form_method_name = '.fromCmykF' | |
c = self.cyanF() | |
m = self.magentaF() | |
y = self.yellowF() | |
k = self.blackF() | |
components = [ | |
(c, round(c * 65535), 257), | |
(m, round(m * 65535), 257), | |
(y, round(y * 65535), 257), | |
(k, round(k * 65535), 257), | |
(self.alphaF(), a64, 257), | |
] | |
elif spec == QtGui.QColor.Spec.Hsl: | |
short_form_method_name = '.fromHsl' | |
precise_form_method_name = '.fromHslF' | |
h = self.hslHueF() | |
s = self.hslSaturationF() | |
l = self.lightnessF() | |
components = [ | |
(h, round(h * 36000), 100), | |
(s, round(s * 65535), 257), | |
(l, round(l * 65535), 257), | |
(self.alphaF(), a64, 257), | |
] | |
else: | |
raise ValueError(f'Unrecognized color spec: {spec}') | |
if a64 == 65535: | |
# alpha is always the last component. If it's at max, we can | |
# safely discard it | |
components.pop() | |
# Choose between short form and long form, and select the | |
# appropriate method name and component values | |
if all(b % c == 0 for (a, b, c) in components): | |
method_name = short_form_method_name | |
component_values = [b // c for (a, b, c) in components] | |
else: | |
method_name = precise_form_method_name | |
component_values = [a for (a, b, c) in components] | |
return f'{PREFIX}{method_name}({", ".join(str(c) for c in component_values)})' | |
from PyQt6 import QtGui | |
QtGui.QColor.__repr__ = QColor_repr |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import PyQt6 | |
from PyQt6 import QtCore, QtGui, QtWidgets | |
import qcolor_repr | |
# These are pretty messy and don't cover every single case, but they | |
# demonstrate that there are no float-imprecision-like issues that cause | |
# round-tripping through repr() to fail | |
app = QtWidgets.QApplication([]) | |
def test_invalid(): | |
assert repr(QtGui.QColor()) == 'PyQt6.QtGui.QColor()' | |
def test_short_forms(): | |
# Tests: "short forms" | |
# (only testing combinations of the first two arguments, since | |
# arguments 3+ behave the same) | |
for ctor_str, first_arg_range, second_arg_range, num_other_args in [ | |
('PyQt6.QtGui.QColor', 255, 255, 1), | |
('PyQt6.QtGui.QColor.fromHsv', 359, 255, 1), | |
('PyQt6.QtGui.QColor.fromCmyk', 255, 255, 2), | |
('PyQt6.QtGui.QColor.fromHsl', 359, 255, 1), | |
]: | |
ctor = eval(ctor_str) | |
for a1 in range(first_arg_range + 1): | |
for a2 in range(second_arg_range + 1): | |
for alpha in [0, 100, 255]: | |
args = [a1, a2] + [0] * num_other_args + [alpha] | |
if alpha == 255: | |
args.pop() | |
color = ctor(*args) | |
r1 = repr(color) | |
r2 = f'{ctor_str}({", ".join(str(arg) for arg in args)})' | |
print(r1, '->', r2) | |
assert r1 == r2, f'FAIL: {ctor_str} / {args}' | |
def test_precise_forms(): | |
import random | |
precise_form_ctors = [ | |
('PyQt6.QtGui.QColor.fromRgba64', False, 3), | |
('PyQt6.QtGui.QColor.fromHsvF', True, 3), | |
('PyQt6.QtGui.QColor.fromCmykF', True, 4), | |
('PyQt6.QtGui.QColor.fromHslF', True, 3), | |
] | |
for _ in range(100000): | |
ctor_str, uses_floats, num_args = random.choice(precise_form_ctors) | |
if uses_floats: | |
args = [random.random() for _ in range(num_args)] | |
else: | |
args = [random.randrange(65536) for _ in range(num_args)] | |
color_1 = eval(ctor_str)(*args) | |
color_2 = eval(repr(color_1)) | |
print(color_1) | |
assert color_1 == color_2, f'FAIL: {ctor_str} / {args}' | |
def test_extended_rgb(): | |
import random | |
for range_limit in [1, 10, 100, 10000]: | |
for _ in range(100000): | |
args = [random.uniform(-range_limit, range_limit) for _ in range(3)] | |
color_1 = QtGui.QColor.fromRgbF(*args) | |
print(color_1) | |
color_2 = eval(repr(color_1)) | |
assert color_1 == color_2, f'FAIL: {ctor_str} / {args}' | |
test_invalid() | |
test_short_forms() | |
test_precise_forms() | |
test_extended_rgb() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment