diff options
Diffstat (limited to 'thcolor/_ref.py')
-rwxr-xr-x | thcolor/_ref.py | 334 |
1 files changed, 64 insertions, 270 deletions
diff --git a/thcolor/_ref.py b/thcolor/_ref.py index 8a39cbb..9c719a8 100755 --- a/thcolor/_ref.py +++ b/thcolor/_ref.py @@ -1,12 +1,14 @@ #!/usr/bin/env python3 -#****************************************************************************** +#************************************************************************** # Copyright (C) 2019 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr> # This file is part of the thcolor project, which is MIT-licensed. -#****************************************************************************** +#************************************************************************** """ Named color reference, parent class. """ -from inspect import (getfullargspec as _getfullargspec, - getmembers as _getmembers, ismethod as _ismethod) +from copy import copy as _copy +from inspect import (getfullargspec as _getfullargspec, getmro as _getmro, + getmembers as _getmembers, ismethod as _ismethod, + isfunction as _isfunction) from itertools import count as _count from warnings import warn as _warn @@ -21,6 +23,7 @@ __all__ = ["Reference"] _default_reference = None _color_cls = None + def _get_color_class(): global _color_cls @@ -31,249 +34,63 @@ def _get_color_class(): _color_cls = Color return _color_cls -class _type_or: - """ A type or another. """ - - def __init__(self, type1, type2): - self._type1 = type1 - self._type2 = type2 - - @property - def type1(self): - return self._type1 - - @property - def type2(self): - return self._type2 - - def __repr__(self): - return f"{repr(self._type1)} | {repr(self._type2)}" - - def __str__(self): - return f"{self._type1} or {self._type2}" - - def __or__(self, other): - return type_or(self, other) - - def __contains__(self, other): - return other in self._type1 or other in self._type2 # --- # Main reference definition. # --- + class Reference: """ Function reference for color parsing and operations. """ - def __init__(self): - pass - - # --- - # Base type and function definitions for parsing. - # --- - - class base_type(type): - """ The metaclass for all types used below. """ - - def __new__(mcls, name, bases, attrs): - return super().__new__(mcls, name, bases, attrs) - - def __init__(self, name, bases, attrs): - self.__name = name - - def __contains__(self, other): - return self if other == self else None - - def __or__(self, other): - return _type_or(self, other) - - def __repr__(self): - return f"<class {repr(self.__name)}>" - - def __str__(self): - return self.__name - - class number(metaclass = base_type): - """ Syntaxical element for expression decoding, representing - a number, integer or decimal, positive or negative. - - This element is usually used to represent a byte value, - a factor (usually from 0.0 to 1.0), a color or an angle - in degrees. """ - - def __init__(self, value): - if type(value) == str: - self._strvalue = value - self._value = float(value) - else: - self._value = float(value) - if self._value == int(self._value): - self._strvalue = str(int(self._value)) - else: - self._strvalue = str(value) - - def __str__(self): - return self._strvalue - - @property - def value(self): - return self._value - - def to_byte(self): - """ Make a byte (from 0 to 255) out of the number. """ - - try: - value = int(self._value) - - assert value == self._value - assert 0 <= value < 256 - except: - raise ValueError("unsuitable value for byte conversion: " \ - f"{repr(self._value)}") - - return value - - def to_factor(self, min = 0.0, max = 1.0): - """ Make a factor (usually from 0.0 to 1.0) out of the number. """ - - if (min is not None and self._value < min) \ - or (max is not None and self._value > max): - if max is None: - msg = f"above {min}" - elif min is None: - msg = f"under {max}" - else: - msg = f"between {min} and {max}" - - raise ValueError(f"expected a value {msg}, got {self._value}") - - try: - assert 0.0 <= self._value <= 1.0 - except: - raise ValueError("expected a value between 0.0 and 1.0, got " \ - f"{self._value}") - - return self._value + def __getattr__(self, name): + def pred(x): + return _isfunction(x) | _ismethod(x) - def to_hue(self): - """ Make an angle in degrees out of the number. """ + for member_name in dir(self): + # Check if it's an interesting member. - return _Angle(_Angle.Type.DEG, self._value) + if member_name[:1] == '_': + continue - class percentage(metaclass = base_type): - """ Syntaxical element for expression decoding, representing - a percentage (number followed by a '%' sign). + member = getattr(self, member_name) + if not _ismethod(member) and not _isfunction(member): + continue - This element is usually used to represent a factor (usually - from 0% to 100%) or anything that can be factored such as a - byte value (where 0% represents 0 and 100% represents 255). """ + # Access the aliases through the mro using self and super. - def __init__(self, value): - self._value = value + for obj in (self, super()): + # Check the aliases. - if value < 0: - value = 0 - - # value can actually be more than 100. - - def __repr__(self): - return f"{self._value} %" - - def to_factor(self, min = 0.0, max = 1.0): - """ Make a factor (usually from 0.0 to 1.0) out of the number. """ + try: + aliases = getattr(obj, member_name)._ref_aliases + except (AttributeError, AssertionError): + continue - value = self._value / 100 - if (min is not None and value < min) \ - or (max is not None and value > max): - if max is None: - msg = f"above {min * 100}%" - elif min is None: - msg = f"under {max * 100}%" - else: - msg = f"between {min * 100}% and {max * 100}%" + for alias_name, alias in aliases: + if alias_name == name: + # XXX: test + print(alias(self.number(1), self.number(2), + self.number(3))) + return alias - raise ValueError(f"expected a percentage {msg}, " \ - f"got {self._value}%") + # Explore the aliases in a recursive way. + try: + value = super().__getattr__(name) return value + except AttributeError: + pass - def to_byte(self): - """ Make a byte (from 0 to 255) out of the number. """ - - if self._value < 0: - self._value = 0 - if self._value > 100: - self._value = 100 - - return int(min(self._value / 100, 1.0) * 255) - - class angle(metaclass = base_type): - """ Syntaxical element for expression decoding, representing - an angle (a number followed by an angle unit: degrees, gradiants, - radiants or turns). """ - - def __init__(self, value): - if not isinstance(value, _Angle): - raise TypeError("expected an Angle instance") - - self._value = value - - def __repr__(self): - return repr(self._value) - - def to_hue(self): - """ Get the :class:`thcolor.Angle` object. """ - - return self._value - - class color(metaclass = base_type): - """ Syntaxical element for expression decoding, representing - a color using several methods: - - - a hexadecimal code preceeded by a '#', e.g. "#123ABC". - - a natural color (see - `NCol <https://www.w3schools.com/colors/colors_ncol.asp>`_). - - a color name as defined by a :class:`Reference`. - - a legacy color name using the legacy color algorithm (or - 'Netscape' color algorithm). """ - - def __init__(self, value): - if not isinstance(value, _get_color_class()): - raise ValueError("expected a Color instance") - - self._value = value - - def __repr__(self): - return repr(self._value) - - def to_color(self): - """ Get the :class:`thcolor.Color` object. """ - - return self._value - - # --- - # Function helper. - # --- - - def alias(*names): - """ Decorator for aliasing with another name. Defines a lot, you - know. """ - - def _decorator(func): - if not hasattr(func, '_ref_aliases'): - func._ref_aliases = () - - func._ref_aliases += names - return func - - return _decorator + raise AttributeError(repr(name)) from None # --- # Function and named color getters. # --- def _get_functions(self): - """ Function getter, which can be used as ``.functions[name](args…)``. + """ Function getter, which can be used as + ``.functions[name](args…)``. Provides a wrapper to the method which checks the argument types and perform the necessary conversions before passing @@ -292,42 +109,18 @@ class Reference: def __getitem__(self, name): fref = self._fref - found = False - # First, check if the function name is valid and if the - # method exists. + # Get the method through __getattr__, who will handle the + # recursivity through parent classes up to Reference. - members = dict(_getmembers(fref, predicate = _ismethod)) - validname = lambda n: type(n) == str and n[0:1] != '_' \ - and n not in ('functions', 'named', 'default', 'alias') - - if validname(name): - try: - method = members[name] - found = True - except (KeyError, AssertionError): - pass - - # Then, if we haven't found the method, check the aliases. - - if not found: - for member in members.values(): - try: - aliases = member._ref_aliases - except (AttributeError, AssertionError): - continue - - if name not in aliases: - continue - method = member - found = True - break - - # If we still haven't found the method, well… time to raise - # the not found exception. - - if not found: - raise KeyError(repr(name)) + def validname(n): + return type(n) == str and n[0:1] != '_' and n \ + not in ('functions', 'named', 'default', 'alias') + try: + assert validname(name) + method = getattr(fref, name) + except (AssertionError, AttributeError): + raise KeyError(name) # Make a function separated from the class, copy the # annotations and add the type check on each argument. @@ -341,7 +134,7 @@ class Reference: self.__annotations__ = func.__annotations__ try: del self.__annotations__['self'] - except: + except AssertionError: pass spec = _getfullargspec(func) @@ -349,7 +142,7 @@ class Reference: def annotate(arg_name): try: return spec.annotations[arg_name] - except: + except (AttributeError, KeyError, IndexError): return None self._args = list(map(annotate, spec.args[1:])) @@ -387,7 +180,7 @@ class Reference: if Reference.color in exp: try: args[-1] = self._fref.colors[arg] - except: + except (IndexError, KeyError): pass else: continue @@ -406,12 +199,12 @@ class Reference: For example, with a classical CSS reference named ``ref``: >>> ref.colors['blue'] - ... Color(type = Color.Type.RGB, red = 0, green = 0, """ \ - """blue = 255, alpha = 1.0) + ... Color(type = Color.Type.RGB, red = 0, """ \ + """green = 0, blue = 255, alpha = 1.0) >>> ref.colors[thcolor.Reference.color(""" \ """thcolor.Color.from_text('#123456'))] - ... Color(type = Color.Type.RGB, red = 18, green = 52, """ \ - """blue = 86, alpha = 1.0) """ + ... Color(type = Color.Type.RGB, red = 18, """ \ + """green = 52, blue = 86, alpha = 1.0) """ class _ColorGetter: def __init__(self, ref): @@ -425,7 +218,7 @@ class Reference: try: name = str(key) - except: + except (TypeError, ValueError): raise KeyError(repr(key)) try: @@ -433,23 +226,24 @@ class Reference: except KeyError: pass except Exception as e: - _warn(RuntimeWarning, f"{self.__class__.__name__} " \ - f"returned exception {e.__class__.__name__} instead " \ - f"of KeyError for color name {repr(name)}.") + _warn(RuntimeWarning, f"{self.__class__.__name__} " + f"returned exception {e.__class__.__name__} " + f"instead of KeyError for color name " + f"{repr(name)}.") pass else: try: assert isinstance(value, _get_color_class()) except AssertionError: - _warn(RuntimeWarning, f"{self.__class__.__name__} " \ - f"returned non-Color value {repr(value)} for " \ + _warn(RuntimeWarning, f"{self.__class__.__name__} " + f"returned non-Color value {repr(value)} for " f"color name {repr(name)}, ignoring.") else: return value try: r, g, b = _netscape_color(name) - except: + except (TypeError, ValueError): pass else: Color = _get_color_class() |