aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThomas Touhey <thomas@touhey.fr>2019-05-11 00:43:31 +0200
committerThomas Touhey <thomas@touhey.fr>2019-05-11 00:43:31 +0200
commitd51ccfd822eec808c6d1283ad696af546f525d3f (patch)
tree15330b1f033546d9ba76e857de38de8b8252ba12
parentfe5f70464d1abf5a0c7d7decab1f376887e944f3 (diff)
Enhanced the docs (part 2).
-rw-r--r--docs/Makefile9
-rw-r--r--docs/angles.rst4
-rw-r--r--docs/colors.rst6
-rw-r--r--docs/expressions.rst89
-rw-r--r--docs/index.rst1
-rwxr-xr-xtests/test_text.py1
-rwxr-xr-xthcolor/_angle.py6
-rwxr-xr-xthcolor/_builtin/_css.py15
-rwxr-xr-xthcolor/_builtin/_default.py2
-rwxr-xr-xthcolor/_color.py29
-rwxr-xr-xthcolor/_exc.py36
-rwxr-xr-xthcolor/_ref.py142
12 files changed, 297 insertions, 43 deletions
diff --git a/docs/Makefile b/docs/Makefile
index 1db33f2..871993a 100644
--- a/docs/Makefile
+++ b/docs/Makefile
@@ -4,6 +4,7 @@
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
+SPHINXWATCH = sphinx-autobuild
SOURCEDIR = .
BUILDDIR = _build
@@ -20,9 +21,17 @@ help:
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+# Livehtml build.
+livehtml:
+ $(SPHINXWATCH) -b html -z ../thcolor $(SPHINXOPTS) . $(BUILDDIR)/html
+
+.PHONY: livehtml
+
# Send the website content (Linux-only).
show: html
find _build/html -type f -exec chmod 644 {} \;
rsync -Prlt --delete _build/html/ "$(WEBROOT)"
.PHONY: show
+
+# End of file.
diff --git a/docs/angles.rst b/docs/angles.rst
index cb63f14..71d52a9 100644
--- a/docs/angles.rst
+++ b/docs/angles.rst
@@ -1,5 +1,5 @@
-Managing angles
-===============
+Angles
+======
Some color representations use angles as some of their properties. Angles can
have one of the following types:
diff --git a/docs/colors.rst b/docs/colors.rst
index a0ac8c8..5a657cd 100644
--- a/docs/colors.rst
+++ b/docs/colors.rst
@@ -1,5 +1,5 @@
-Managing colors
-===============
+Colors
+======
Colors can have one of the following types:
@@ -8,4 +8,4 @@ Colors can have one of the following types:
Colors are represented in ``thcolor`` as instances of the following class:
.. autoclass:: thcolor.Color
- :members: from_text, type, rgb, rgba, hls, hlsa, hwb, hwba, css
+ :members: type, rgb, rgba, hls, hlsa, hwb, hwba, css
diff --git a/docs/expressions.rst b/docs/expressions.rst
new file mode 100644
index 0000000..6f9e645
--- /dev/null
+++ b/docs/expressions.rst
@@ -0,0 +1,89 @@
+.. _expr:
+
+Expressions
+===========
+
+One of the aims of the ``thcolor`` module was to decode text-based expressions
+representing colors with possibilities challenging and even outdo
+CSS color expression possibilities. This is what the following static method
+is for:
+
+.. automethod:: thcolor.Color.from_text
+
+Expression concepts
+-------------------
+
+The goal of these expressions was to embrace and extend CSS syntax, so they
+are basically either basic expressions or function calls, with the following
+argument types:
+
+.. autoclass:: thcolor.Reference.number
+ :members:
+
+.. autoclass:: thcolor.Reference.percentage
+ :members:
+
+.. autoclass:: thcolor.Reference.angle
+ :members:
+
+.. autoclass:: thcolor.Reference.color
+ :members:
+
+These elements are separated by separators (either commas, slashes, or simple
+spaces) and can be passed to functions, and the calls themselves can be passed
+to other functions. A function call is made in the following fashion:
+
+::
+
+ <function name>(<number | percentage | angle | color> [<separator> …])
+
+If at least one separator (even simple spaces) are required between arguments,
+extraneous separators between and after the arguments are ignored. Other than
+if spaces are used as separators, spaces around the parenthesis or the
+separators (and "between" the separators as spaces are recognized as
+separators) are ignored.
+
+Here are some example calls:
+
+::
+
+ rgb(1, 2, 3)
+ rgb ( 1 22 //// 242 , 50.0% ,/,)
+ hsl (0 1 50 % / 22)
+ gray ( red( #123456 )/0.2/)
+
+In case of incorrectly formatted string, the following exception is returned:
+
+.. autoexception:: thcolor.ColorExpressionDecodingError
+ :members:
+
+Defining a reference
+--------------------
+
+Functions and color names are defined in a reference:
+
+- color names are defined behind an overload of
+ :meth:`thcolor.Reference._color`.
+- functions are defined as reference class methods and use `type hints
+ <https://www.python.org/dev/peps/pep-0484/>`_ to describe the types
+ they are expecting.
+
+The reference must be a derivative of the following class:
+
+.. autoclass:: thcolor.Reference
+ :members: _color, functions, colors, default
+
+Builtin references
+------------------
+
+The following references are defined:
+
+.. autoclass:: thcolor.CSS1Reference
+
+.. autoclass:: thcolor.CSS2Reference
+
+.. autoclass:: thcolor.CSS3Reference
+
+.. autoclass:: thcolor.CSS4Reference
+
+.. autoclass:: thcolor.DefaultReference
diff --git a/docs/index.rst b/docs/index.rst
index f417c41..0db7bf0 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -24,6 +24,7 @@ Sommaire
angles
colors
+ expressions
.. _Thomas Touhey: https://thomas.touhey.fr/
.. _textoutpc: https://textout.touhey.pro/
diff --git a/tests/test_text.py b/tests/test_text.py
index 22bfb2a..055d815 100755
--- a/tests/test_text.py
+++ b/tests/test_text.py
@@ -20,6 +20,7 @@ def _deg(value):
('chucknorris', (192, 0, 0, 1.00)),
('rgb(1, 22,242)', ( 1, 22, 242, 1.00)),
(' rgb (1,22, 242 , 50.0% )', ( 1, 22, 242, 0.50)),
+ (' rgb (1 22/ /242,50.0%,/)', ( 1, 22, 242, 0.50)),
('rgba(1,22,242,0.500)', ( 1, 22, 242, 0.50)),
('rbga(5, 7)', ( 5, 0, 7, 1.00)),
('hsl(0, 1,50.0%)', (255, 0, 0, 1.00)),
diff --git a/thcolor/_angle.py b/thcolor/_angle.py
index dbcb6f8..ac3198d 100755
--- a/thcolor/_angle.py
+++ b/thcolor/_angle.py
@@ -256,6 +256,8 @@ class Angle:
""" The read-only angle value in degrees. If the angle isn't in degrees
already, it will be converted automatically. """
+ if self._type == Angle.Type.DEG:
+ return self._value
return self.turns * 360
@property
@@ -263,6 +265,8 @@ class Angle:
""" The read-only angle value in gradiants. If the angle isn't in
gradiants already, it will be converted automatically. """
+ if self._type == Angle.Type.GRAD:
+ return self._value
return self.turns * 400
@property
@@ -270,6 +274,8 @@ class Angle:
""" The read-only angle value in radiants. If the angle isn't in
radiants already, it will be converted automatically. """
+ if self._type == Angle.Type.RAD:
+ return self._value
return self.turns * (2 * _pi)
@property
diff --git a/thcolor/_builtin/_css.py b/thcolor/_builtin/_css.py
index 6b49ff9..5cf012c 100755
--- a/thcolor/_builtin/_css.py
+++ b/thcolor/_builtin/_css.py
@@ -21,8 +21,7 @@ def _rgb(raw):
return _Color(_Color.Type.RGB, r, g, b)
class CSS1Reference(_Reference):
- """ Named colors from CSS Level 1:
- https://www.w3.org/TR/CSS1/ """
+ """ Named colors from `CSS Level 1 <https://www.w3.org/TR/CSS1/>`_. """
number = _Reference.number
percentage = _Reference.percentage
@@ -103,8 +102,8 @@ class CSS1Reference(_Reference):
return self._rgb((r, g, b, 1.0), (0, 1, 2))
class CSS2Reference(CSS1Reference):
- """ Named colors from CSS Level 2 (Revision 1):
- https://www.w3.org/TR/CSS2/ """
+ """ Named colors from `CSS Level 2 (Revision 1)
+ <https://www.w3.org/TR/CSS2/>`_. """
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -123,8 +122,8 @@ class CSS2Reference(CSS1Reference):
return super()._color(name)
class CSS3Reference(CSS2Reference):
- """ Named colors from CSS Color Module Level 3:
- https://drafts.csswg.org/css-color-3/ """
+ """ Named colors and functions from `CSS Color Module Level 3
+ <https://drafts.csswg.org/css-color-3/>`_. """
number = _Reference.number
percentage = _Reference.percentage
@@ -329,8 +328,8 @@ class CSS3Reference(CSS2Reference):
return self._hsl((h, s, l, alpha), (0, 1, 2))
class CSS4Reference(CSS3Reference):
- """ Named colors from CSS Color Module Level 4:
- https://drafts.csswg.org/css-color/ """
+ """ Named colors and functions from `CSS Color Module Level 4
+ <https://drafts.csswg.org/css-color/>`_. """
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
diff --git a/thcolor/_builtin/_default.py b/thcolor/_builtin/_default.py
index b047519..05ac49e 100755
--- a/thcolor/_builtin/_default.py
+++ b/thcolor/_builtin/_default.py
@@ -13,7 +13,7 @@ from ._css import CSS4Reference as _CSS4Reference
__all__ = ["DefaultReference"]
class DefaultReference(_CSS4Reference):
- """ Extensions to the CSS Color Module Level 4 reference. """
+ """ Functions extending the CSS Color Module Level 4 reference. """
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
diff --git a/thcolor/_color.py b/thcolor/_color.py
index d273071..a26e929 100755
--- a/thcolor/_color.py
+++ b/thcolor/_color.py
@@ -59,7 +59,7 @@ def _get_color_pattern():
( \s* \( \s* (?P<arg> (?0)? ) \s* \) )?)
)
- \s* ((?P<sep>[,/\s]) \s* (?P<nextargs> (?0))?)?
+ \s* ((?P<sep>[,/\s])+ \s* (?P<nextargs> (?0))?)?
""", _re.VERBOSE | _re.I | _re.M)
return _color_pattern
@@ -116,7 +116,8 @@ class Color:
An alpha value going from 0.0 (invisible) to 1.0 (opaque) can be
appended to the base components.
- .. function:: Color(Color.Type.HSL, hue, saturation, lightness, alpha = 1.0)
+ .. function:: Color(Color.Type.HSL, hue, saturation, lightness, """ \
+ """alpha = 1.0)
Create a color using its hue, saturation and lightness components.
The hue is represented by an :class:`Angle` object, and the
@@ -125,7 +126,8 @@ class Color:
An alpha value going from 0.0 (invisible) to 1.0 (opaque) can be
appended to the base components.
- .. function:: Color(Color.Type.HWB, hue, whiteness, blackness, alpha = 1.0)
+ .. function:: Color(Color.Type.HWB, hue, whiteness, blackness, """ \
+ """alpha = 1.0)
Create a color using its hue, whiteness and blackness components.
The hue is represented by an :class:`Angle` object, and the
@@ -195,6 +197,19 @@ class Color:
argtext = ', '.join(f'{key} = {value}' for key, value in args)
return f"{self.__class__.__name__}({argtext})"
+ def __eq__(self, other):
+ if not isinstance(other, Color):
+ return super().__eq__(other)
+
+ if other.type == Color.Type.INVALID:
+ return self._type == Color.Type.INVALID
+ elif other.type == Color.Type.HSL:
+ return self.hsla() == other.hsla()
+ elif other.type == Color.Type.HWB:
+ return self.hwba() == other.hwba()
+
+ return self.rgba() == other.rgba()
+
# ---
# Management methods.
# ---
@@ -309,7 +324,6 @@ class Color:
return self._type
- @property
def alpha(self):
""" Get the alpha. """
@@ -626,9 +640,8 @@ class Color:
An example:
>>> Color.from_text("#123456")
- ... Color(type = Color.Type.RGB, red = 18, green = 52, blue = 86, alpha = 1.0)
-
- TODO: describe the text format here. """
+ ... Color(type = Color.Type.RGB, red = 18, green = 52, """ \
+ """ blue = 86, alpha = 1.0) """
if ref is None:
ref = _Reference.default()
@@ -812,7 +825,7 @@ class Color:
result = results[0].value
try:
- result = result.to_color()
+ result = ref.colors[result]
except AttributeError:
raise _ColorExpressionDecodingError("expected a color",
column = column)
diff --git a/thcolor/_exc.py b/thcolor/_exc.py
index 1d858f2..f4954d8 100755
--- a/thcolor/_exc.py
+++ b/thcolor/_exc.py
@@ -17,9 +17,14 @@ class ColorExpressionDecodingError(Exception):
""" A color decoding error has occurred on the text. """
def __init__(self, text, column = None, func = None):
- self._column = column
- self._func = func
- self._text = text
+ try:
+ self._column = column
+ assert self._column >= 0
+ except:
+ self._column = None
+
+ self._func = str(func)
+ self._text = str(text)
def __str__(self):
msg = ""
@@ -29,12 +34,35 @@ class ColorExpressionDecodingError(Exception):
if self._func is not None:
msg += ", "
if self._func is not None:
- msg += f"in function {repr(self._func)}"
+ msg += f"for function {repr(self._func)}"
if msg:
msg += ": "
return msg + self._text
+ @property
+ def text(self):
+ """ Exception message, usually linked to the context. """
+
+ return self._text
+
+ @property
+ def column(self):
+ """ Column of the expression at which the exception has occurred,
+ ``None`` if the error has occurred on an unknown column or on the
+ whole exception. """
+
+ return self._column
+
+ @property
+ def func(self):
+ """ Function name we were calling when the error has occurred,
+ either on arguments decoding or erroneous argument type or
+ value, ``None`` if the context is unknown or the error hasn't
+ occurred while calling a function or decoding its arguments. """
+
+ return self._func
+
# ---
# Internal exceptions.
# ---
diff --git a/thcolor/_ref.py b/thcolor/_ref.py
index 19a469c..beb97fe 100755
--- a/thcolor/_ref.py
+++ b/thcolor/_ref.py
@@ -8,6 +8,7 @@
from inspect import (getfullargspec as _getfullargspec,
getmembers as _getmembers, ismethod as _ismethod)
from itertools import count as _count
+from warnings import warn as _warn
from ._angle import Angle as _Angle
from ._sys import netscape_color as _netscape_color
@@ -93,7 +94,12 @@ class Reference:
return self.__name
class number(metaclass = base_type):
- """ The number 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:
@@ -106,7 +112,12 @@ class Reference:
else:
self._strvalue = str(value)
+ def __str__(self):
+ return self._strvalue
+
def to_byte(self):
+ """ Make a byte (from 0 to 255) out of the number. """
+
try:
value = int(self._value)
@@ -119,6 +130,8 @@ class Reference:
return value
def to_factor(self):
+ """ Make a factor (usually from 0.0 to 1.0) out of the number. """
+
try:
assert 0.0 <= self._value <= 1.0
except:
@@ -127,15 +140,19 @@ class Reference:
return self._value
- def to_color(self):
- r, g, b = _netscape_color(self._strvalue)
- Color = _get_color_class()
- return Color(Color.Type.RGB, r, g, b, 1.0)
-
def to_hue(self):
+ """ Make an angle in degrees out of the number. """
+
return _Angle(_Angle.Type.DEG, self._value)
class percentage(metaclass = base_type):
+ """ Syntaxical element for expression decoding, representing
+ a percentage (number followed by a '%' sign).
+
+ 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). """
+
def __init__(self, value):
self._value = value
@@ -147,10 +164,9 @@ class Reference:
def __repr__(self):
return f"{self._value} %"
- def to_byte(self):
- return int(min(self._value / 100, 1.0) * 255)
-
def to_factor(self):
+ """ Make a factor (usually from 0.0 to 1.0) out of the number. """
+
try:
assert 0 <= self._value <= 100
except:
@@ -159,7 +175,21 @@ class Reference:
return self._value / 100
+ 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")
@@ -170,9 +200,21 @@ class Reference:
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")
@@ -183,6 +225,8 @@ class Reference:
return repr(self._value)
def to_color(self):
+ """ Get the :class:`thcolor.Color` object. """
+
return self._value
# ---
@@ -190,8 +234,18 @@ class Reference:
# ---
def _get_functions(self):
- """ The functions getter, for getting a function using its
- name. """
+ """ 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
+ them to the functions.
+
+ For example, with a classical CSS reference named ``ref``:
+
+ >>> ref.functions['rgb'](ref.number(18), ref.number(52), """ \
+ """ref.number(86))
+ ... Color(type = Color.Type.RGB, red = 18, green = 52, """ \
+ """blue = 86, alpha = 1.0) """
class _FunctionGetter:
def __init__(self, ref):
@@ -270,7 +324,7 @@ class Reference:
if Reference.color in exp:
try:
- args[-1] = arg.to_color()
+ args[-1] = self._fref.colors[arg]
except:
pass
else:
@@ -286,14 +340,60 @@ class Reference:
return _FunctionGetter(self)
def _get_colors(self):
- """ The colors getter, for getting a named color. """
+ """ Colors getter, which can be used as ``.colors[value]``.
+ 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)
+ >>> ref.colors[thcolor.Reference.color(""" \
+ """thcolor.Color.from_text('#123456'))]
+ ... Color(type = Color.Type.RGB, red = 18, green = 52, """ \
+ """blue = 86, alpha = 1.0) """
class _ColorGetter:
def __init__(self, ref):
self._cref = ref
- def __getitem__(self, name):
- return self._cref._color(name)
+ def __getitem__(self, key):
+ try:
+ return key.to_color()
+ except AttributeError:
+ pass
+
+ try:
+ name = str(key)
+ except:
+ raise KeyError(repr(key))
+
+ try:
+ value = self._cref._color(name)
+ 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)}.")
+ 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 " \
+ f"color name {repr(name)}, ignoring.")
+ else:
+ return value
+
+ try:
+ r, g, b = _netscape_color(name)
+ except:
+ pass
+ else:
+ Color = _get_color_class()
+ return Color(Color.Type.RGB, r, g, b)
+
+ raise KeyError(repr(key))
return _ColorGetter(self)
@@ -305,12 +405,20 @@ class Reference:
# ---
def _color(self, name):
- """ Get a named color. """
+ """ Name color getter used behind the
+ :attr:`thcolor.Reference.colors` getter, ought to be overriden
+ by superseeding classes.
+
+ These classes should return ``super()._color(name)`` in case
+ they have no match for the given name. """
raise KeyError(f'{name}: no such color')
def default():
- """ Get the default reference. """
+ """ Static method for gathering the default reference, by
+ default :class:`thcolor.DefaultReference`. Is only used on
+ the base :class:`thcolor.Reference` type and shall not be
+ overriden. """
global _default_reference