aboutsummaryrefslogtreecommitdiff
path: root/thcolor/_ref.py
diff options
context:
space:
mode:
Diffstat (limited to 'thcolor/_ref.py')
-rwxr-xr-xthcolor/_ref.py334
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()