Skip to content

Instantly share code, notes, and snippets.

@RoadrunnerWMC
Created April 5, 2023 04:56
Show Gist options
  • Save RoadrunnerWMC/c7ba91a8f31bb1cd9b99b8926d27552c to your computer and use it in GitHub Desktop.
Save RoadrunnerWMC/c7ba91a8f31bb1cd9b99b8926d27552c to your computer and use it in GitHub Desktop.
A proof-of-concept for a __repr__ function for PyQt QColor
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
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