# builtins import warnings import random import datetime import colorsys 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 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 def prettydate(value): if isinstance(value, datetime.datetime): return value.strftime('%a %b %d %Y - %H:%M:%S') return 'Lost in Time' class ChildAwareMeta(type): def __init__(cls, *args, **kwargs): super().__init__(*args, **kwargs) #super().__init_subclass__(**kwargs) cls.__class_children__ = {} cls.__class_descendants__ = {} cls.__class_children_lowercase__ = {} cls.__class_descendants_lowercase__ = {} cls._lowerclass = cls.__name__.lower() for base in cls.__bases__: if hasattr(base, '__class_children__'): # in lieu of isinstance check with as-yet undefined ChildAware if cls.__name__ in base.__class_children__: cls_old = base.__class_children__[cls.__name__] # get existing class from base # 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__'.") base.__class_children__[cls.__name__] = cls 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 for ancestor in cls.__mro__[1:]: if hasattr(ancestor, '__class_descendants__'): # in lieu of isinstance check with as-yet undefined ChildAware if cls.__name__ in ancestor.__class_descendants__: cls_old = ancestor.__class_descendants__[cls.__name__] # get existing class from ancestor # 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 '{ancestor.__full_qualname__}.__class_descendants__' .") ancestor.__class_descendants__[cls.__name__] = cls 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 @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__}' class ChildAware(metaclass=ChildAwareMeta): pass 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: alpha = 1.0 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: value = 1.0 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 ) def css(self): """ newstyle rgb(), NOT rgba(). """ return f'rgb({round(self.red * 255)} {round(self.green * 255)} {round(self.blue * 255)} / {self.alpha * 100}%)' 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) 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) 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)