2024-05-20 02:10:17 +00:00
# builtins
2024-04-17 17:30:52 +00:00
import warnings
2024-04-29 16:16:00 +00:00
import random
2024-05-02 18:57:38 +00:00
import datetime
2024-05-17 00:56:17 +00:00
import colorsys
2024-04-29 16:16:00 +00:00
def random_string_light ( length = 6 ) :
ranges = ( ( 65 , 90 ) , ( 97 , 122 ) ) # A-Z, a-z
rand = random . SystemRandom ( ) #os.urandom, cryptographically secure
string = ' '
for i in range ( 0 , length ) :
r = ranges [ rand . randint ( 0 , len ( ranges ) - 1 ) ]
string + = chr ( rand . randint ( r [ 0 ] , r [ 1 ] ) )
return string
2024-04-17 17:30:52 +00:00
2024-04-12 19:22:00 +00:00
class class_or_instance_method ( classmethod ) :
def __get__ ( self , instance , owner = None ) :
if instance is None : # call via class
return super ( ) . __get__ ( instance , owner ) # invoke classmethod.__get__
return self . __func__ . __get__ ( instance , owner ) # invoke standard implementation
2024-05-02 18:57:38 +00:00
def prettydate ( value ) :
if isinstance ( value , datetime . datetime ) :
return value . strftime ( ' %a % b %d % Y - % H: % M: % S ' )
return ' Lost in Time '
2024-04-12 19:22:00 +00:00
class ChildAwareMeta ( type ) :
def __init__ ( cls , * args , * * kwargs ) :
super ( ) . __init__ ( * args , * * kwargs )
#super().__init_subclass__(**kwargs)
cls . __class_children__ = { }
cls . __class_descendants__ = { }
2024-05-20 17:30:21 +00:00
cls . __class_children_lowercase__ = { }
cls . __class_descendants_lowercase__ = { }
2024-04-12 19:22:00 +00:00
cls . _lowerclass = cls . __name__ . lower ( )
for base in cls . __bases__ :
2024-04-17 17:30:52 +00:00
if hasattr ( base , ' __class_children__ ' ) : # in lieu of isinstance check with as-yet undefined ChildAware
2024-04-12 19:22:00 +00:00
if cls . __name__ in base . __class_children__ :
2024-04-17 17:30:52 +00:00
2024-05-20 17:30:21 +00:00
cls_old = base . __class_children__ [ cls . __name__ ] # get existing class from base
2024-04-17 17:30:52 +00:00
2024-05-20 17:30:21 +00:00
# same qualname means redefinition of class in local scope,
# which is a-ok and can freely overwrite the old reference
if cls . __full_qualname__ != cls_old . __full_qualname__ :
warnings . warn ( f " ChildAware subclasses should have unique names, ' { cls . __full_qualname__ } ' overwrites ' { cls_old . __full_qualname__ } ' in ' { base . __full_qualname__ } .__class_children__ ' . " )
2024-04-17 17:30:52 +00:00
2024-04-12 19:22:00 +00:00
base . __class_children__ [ cls . __name__ ] = cls
2024-05-20 17:30:21 +00:00
if cls . _lowerclass in base . __class_children_lowercase__ :
cls_old = base . __class_children_lowercase__ [ cls . _lowerclass ]
# same qualname yadda yadda
if cls . __full_qualname__ != cls_old . __full_qualname__ :
warnings . warn ( f " ChildAware subclasses should have unique lowercase names, ' { cls . __full_qualname__ } ' overwrites ' { cls_old . __full_qualname__ } ' in ' { base . __full_qualname__ } .__class_children_lowercase__ ' . " )
base . __class_children_lowercase__ [ cls . _lowerclass ] = cls
2024-04-12 19:22:00 +00:00
for ancestor in cls . __mro__ [ 1 : ] :
2024-04-17 17:30:52 +00:00
if hasattr ( ancestor , ' __class_descendants__ ' ) : # in lieu of isinstance check with as-yet undefined ChildAware
2024-04-12 19:22:00 +00:00
if cls . __name__ in ancestor . __class_descendants__ :
2024-04-17 17:30:52 +00:00
2024-05-20 17:30:21 +00:00
cls_old = ancestor . __class_descendants__ [ cls . __name__ ] # get existing class from ancestor
2024-04-17 17:30:52 +00:00
2024-05-20 17:30:21 +00:00
# same qualname means redefinition of class in local scope,
# which is a-ok and can freely overwrite the old reference
if cls . __full_qualname__ != cls_old . __full_qualname__ :
2024-04-17 17:30:52 +00:00
warnings . warn ( f " ChildAware subclasses should have unique names, ' { cls . __full_qualname__ } ' overwrites ' { cls_old . __full_qualname__ } ' in ' { ancestor . __full_qualname__ } .__class_descendants__ ' . " )
2024-04-12 19:22:00 +00:00
ancestor . __class_descendants__ [ cls . __name__ ] = cls
2024-05-20 17:30:21 +00:00
if cls . __name__ in ancestor . __class_descendants_lowercase__ :
cls_old = ancestor . __class_descendants_lowercase__ [ cls . _lowerclass ]
if cls . __full_qualname__ != cls_old . __full_qualname__ :
warnings . warn ( f " ChildAware subclasses should have unique lowercase names, ' { cls . __full_qualname__ } ' overwrites ' { cls_old . __full_qualname__ } ' in ' { base . __full_qualname__ } .__class_descendants_lowercase__ ' . " )
ancestor . __class_descendants_lowercase__ [ cls . _lowerclass ] = cls
2024-04-17 17:30:52 +00:00
@property # @property works to make this a *class* attribute because we're in the metaclass
def __full_qualname__ ( cls ) :
return f ' { cls . __module__ } . { cls . __qualname__ } '
2024-04-12 19:22:00 +00:00
class ChildAware ( metaclass = ChildAwareMeta ) :
pass
2024-05-17 00:56:17 +00:00
class Color ( object ) :
"""
Magic color class implementing and supplying on - the - fly manipulation of
RGB and HSV ( and alpha ) attributes . Taken from gulik .
"""
def __init__ ( self , red = None , green = None , blue = None , alpha = None , hue = None , saturation = None , value = None ) :
rgb_passed = bool ( red ) | bool ( green ) | bool ( blue )
hsv_passed = bool ( hue ) | bool ( saturation ) | bool ( value )
if not alpha :
2024-07-31 02:11:42 +00:00
alpha = 1.0
2024-05-17 00:56:17 +00:00
if rgb_passed and hsv_passed :
raise ValueError ( " Color can ' t be initialized with RGB and HSV at the same time. " )
elif hsv_passed :
if not hue :
hue = 0.0
if not saturation :
saturation = 0.0
if not value :
value = 0.0
super ( Color , self ) . __setattr__ ( ' hue ' , hue )
super ( Color , self ) . __setattr__ ( ' saturation ' , saturation )
super ( Color , self ) . __setattr__ ( ' value ' , value )
self . _update_rgb ( )
else :
if not red :
red = 0
if not green :
green = 0
if not blue :
blue = 0
super ( Color , self ) . __setattr__ ( ' red ' , red )
super ( Color , self ) . __setattr__ ( ' green ' , green )
super ( Color , self ) . __setattr__ ( ' blue ' , blue )
self . _update_hsv ( )
super ( Color , self ) . __setattr__ ( ' alpha ' , alpha )
def __setattr__ ( self , key , value ) :
if key in ( ' red ' , ' green ' , ' blue ' ) :
if value > 1.0 :
2024-07-31 02:11:42 +00:00
value = 1.0
2024-05-17 00:56:17 +00:00
super ( Color , self ) . __setattr__ ( key , value )
self . _update_hsv ( )
elif key in ( ' hue ' , ' saturation ' , ' value ' ) :
if key == ' hue ' and ( value > = 360.0 or value < 0 ) :
value = value % 360.0
elif key != ' hue ' and value > 1.0 :
value = 1.0
super ( Color , self ) . __setattr__ ( key , value )
self . _update_rgb ( )
else :
if key == ' alpha ' and value > 1.0 : # TODO: Might this be more fitting in another place?
value = 1.0
super ( Color , self ) . __setattr__ ( key , value )
def __repr__ ( self ) :
return ' < %s : red %f , green %f , blue %f , hue %f , saturation %f , value %f , alpha %f > ' % (
self . __class__ . __name__ ,
self . red ,
self . green ,
self . blue ,
self . hue ,
self . saturation ,
self . value ,
self . alpha
)
2024-09-01 16:51:25 +00:00
def css ( self ) :
2024-07-31 02:11:42 +00:00
"""
newstyle rgb ( ) , NOT rgba ( ) .
"""
2024-09-01 16:51:25 +00:00
return f ' rgb( { round ( self . red * 255 ) } { round ( self . green * 255 ) } { round ( self . blue * 255 ) } / { self . alpha * 100 } %) '
2024-05-17 00:56:17 +00:00
def clone ( self ) :
return Color ( red = self . red , green = self . green , blue = self . blue , alpha = self . alpha )
def blend ( self , other , mode = ' normal ' ) :
clone = self . clone ( )
if clone . alpha != 1.0 : # no clue how to blend with a translucent bottom layer
clone . red = clone . red * clone . alpha
clone . green = clone . green * clone . alpha
clone . blue = clone . blue * clone . alpha
clone . alpha = 1.0
if mode == ' normal ' :
own_influence = 1.0 - other . alpha
clone . red = ( clone . red * own_influence ) + ( other . red * other . alpha )
clone . green = ( clone . green * own_influence ) + ( other . green * other . alpha )
clone . blue = ( clone . blue * own_influence ) + ( other . blue * other . alpha )
return clone
def lighten ( self , other ) :
if isinstance ( other , int ) or isinstance ( other , float ) :
other = Color ( red = other , green = other , blue = other , alpha = 1.0 )
if self . alpha != 1.0 :
self . red = self . red * self . alpha
self . green = self . green * self . alpha
self . blue = self . blue * self . alpha
self . alpha = 1.0
red = self . red + ( other . red * other . alpha )
green = self . green + ( other . green * other . alpha )
blue = self . blue + ( other . blue * other . alpha )
if red > 1.0 :
red = 1.0
if green > 1.0 :
green = 1.0
if blue > 1.0 :
blue = 1.0
self . red = red
self . green = green
self . blue = blue
def darken ( self , other ) :
if isinstance ( other , int ) or isinstance ( other , float ) :
other = Color ( red = other , green = other , blue = other , alpha = 1.0 )
red = self . red - other . red
green = self . green - other . green
blue = self . blue - other . blue
if red < 0 :
red = 0
if green < 0 :
green = 0
if blue < 0 :
blue = 0
self . red = red
self . green = green
self . blue = blue
def tuple_rgb ( self ) :
""" return color (without alpha) as tuple, channels being float 0.0-1.0 """
return ( self . red , self . green , self . blue )
def tuple_rgba ( self ) :
""" return color (*with* alpha) as tuple, channels being float 0.0-1.0 """
return ( self . red , self . green , self . blue , self . alpha )
2024-07-31 02:11:42 +00:00
def hex_rgb ( self ) :
red , green , blue = map ( lambda channel : round ( channel * 255 ) , self . tuple_rgb ( ) )
return f ' # { red : 02x } { green : 02x } { blue : 02x } '
def hex_rgba ( self ) :
red , green , blue , alpha = map ( lambda channel : round ( channel * 255 ) , self . tuple_rgba ( ) )
return f ' # { red : 02x } { green : 02x } { blue : 02x } { alpha : 02x } '
@classmethod
def from_hex ( cls , hexstring ) :
hexstring = hexstring . lower ( )
if hexstring . startswith ( ' # ' ) :
hexstring = hexstring [ 1 : ]
length = len ( hexstring )
if length % 2 != 0 :
raise ValueError ( f " All hex channels MUST be to characters long, got uneven length in ' { hexstring } ' ! " )
if length == 3 :
raise NotImplemented ( " Place code to parse short hex format for colors here. " )
# TODO: Is 4 chars valid shortform for rgba?
if length > 8 :
raise ValueError ( f " More than 4 8-bit channels in hex string: ' { hexstring } ' ! " )
if length < 6 :
raise ValueError ( f " Less than 3 8-bit channels in hex string: ' { hexstring } ' ! " )
channel_names = {
0 : ' red ' ,
2 : ' green ' ,
4 : ' blue ' ,
6 : ' alpha ' ,
}
params = { }
for i in range ( 0 , len ( hexstring ) , 2 ) :
hexbyte = hexstring [ i : i + 2 ]
intbyte = int ( hexbyte , 16 )
channel_name = channel_names [ i ]
params [ channel_name ] = intbyte / 255
return cls ( * * params )
2024-05-17 00:56:17 +00:00
def _update_hsv ( self ) :
hue , saturation , value = colorsys . rgb_to_hsv ( self . red , self . green , self . blue )
super ( Color , self ) . __setattr__ ( ' hue ' , hue * 360.0 )
super ( Color , self ) . __setattr__ ( ' saturation ' , saturation )
super ( Color , self ) . __setattr__ ( ' value ' , value )
def _update_rgb ( self ) :
red , green , blue = colorsys . hsv_to_rgb ( self . hue / 360.0 , self . saturation , self . value )
super ( Color , self ) . __setattr__ ( ' red ' , red )
super ( Color , self ) . __setattr__ ( ' green ' , green )
super ( Color , self ) . __setattr__ ( ' blue ' , blue )