diff options
author | Thomas Touhey <thomas@touhey.fr> | 2019-05-11 00:43:31 +0200 |
---|---|---|
committer | Thomas Touhey <thomas@touhey.fr> | 2019-05-11 00:43:31 +0200 |
commit | d51ccfd822eec808c6d1283ad696af546f525d3f (patch) | |
tree | 15330b1f033546d9ba76e857de38de8b8252ba12 | |
parent | fe5f70464d1abf5a0c7d7decab1f376887e944f3 (diff) |
Enhanced the docs (part 2).
-rw-r--r-- | docs/Makefile | 9 | ||||
-rw-r--r-- | docs/angles.rst | 4 | ||||
-rw-r--r-- | docs/colors.rst | 6 | ||||
-rw-r--r-- | docs/expressions.rst | 89 | ||||
-rw-r--r-- | docs/index.rst | 1 | ||||
-rwxr-xr-x | tests/test_text.py | 1 | ||||
-rwxr-xr-x | thcolor/_angle.py | 6 | ||||
-rwxr-xr-x | thcolor/_builtin/_css.py | 15 | ||||
-rwxr-xr-x | thcolor/_builtin/_default.py | 2 | ||||
-rwxr-xr-x | thcolor/_color.py | 29 | ||||
-rwxr-xr-x | thcolor/_exc.py | 36 | ||||
-rwxr-xr-x | thcolor/_ref.py | 142 |
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 |