From a9d5371c05a3fc239951b5666b4ea79ffd2fec90 Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Tue, 2 Jul 2024 23:08:45 +0200 Subject: [PATCH] Update tests for color specifications --- tests/css-parsing-tests | 2 +- tests/test_tinycss2.py | 299 +++++++++++++++++++++++++++++----------- tinycss2/color3.py | 6 +- tinycss2/color4.py | 24 ++-- 4 files changed, 232 insertions(+), 99 deletions(-) diff --git a/tests/css-parsing-tests b/tests/css-parsing-tests index 30efc00..530ab15 160000 --- a/tests/css-parsing-tests +++ b/tests/css-parsing-tests @@ -1 +1 @@ -Subproject commit 30efc000d5931d7562815b5cd7001e4298563dde +Subproject commit 530ab150796b959240fb09eae3b764d8bae6d182 diff --git a/tests/test_tinycss2.py b/tests/test_tinycss2.py index 539114c..0df3582 100644 --- a/tests/test_tinycss2.py +++ b/tests/test_tinycss2.py @@ -1,7 +1,6 @@ import functools import json import pprint -from colorsys import hls_to_rgb from pathlib import Path import pytest @@ -105,7 +104,7 @@ def test(css, expected): return decorator -SKIP = dict(skip_comments=True, skip_whitespace=True) +SKIP = {'skip_comments': True, 'skip_whitespace': True} @json_test() @@ -153,110 +152,242 @@ def test_nth(input): return parse_nth(input) -@json_test(filename='color.json') -def test_color_parse3(input): - return parse_color3(input) +def _number(value): + if value is None: + return 'none' + value = round(value + 0.0000001, 6) + return str(int(value) if value.is_integer() else value) -@json_test(filename='color.json') -def test_color_common_parse3(input): - return parse_color3(input) +def test_color_currentcolor_3(): + for value in ('currentcolor', 'currentColor', 'CURRENTCOLOR'): + assert parse_color3(value) == 'currentColor' -@json_test(filename='color.json') -def test_color_common_parse4(input): - color = parse_color4(input) - if not color or color == 'currentColor': - return color - elif color.space == 'srgb': - return RGBA(*color) - elif color.space == 'hsl': - rgb = hls_to_rgb(color[0] / 360, color[2] / 100, color[1] / 100) - return RGBA(*rgb, color.alpha) +def test_color_currentcolor_4(): + for value in ('currentcolor', 'currentColor', 'CURRENTCOLOR'): + assert parse_color4(value) == 'currentcolor' + + +@json_test() +def test_color_function_4(input): + if not (color := parse_color4(input)): + return None + (*coordinates, alpha) = color + result = f'color({color.space}' + for coordinate in coordinates: + result += f' {_number(coordinate)}' + if alpha != 1: + result += f' / {_number(alpha)}' + result += ')' + return result @json_test() -def test_color3(input): - return parse_color3(input) +def test_color_hexadecimal_3(input): + if not (color := parse_color3(input)): + return None + (*coordinates, alpha) = color + result = f'rgb{"a" if alpha != 1 else ""}(' + result += f'{", ".join(_number(coordinate * 255) for coordinate in coordinates)}' + if alpha != 1: + result += f', {_number(alpha)}' + result += ')' + return result -@json_test(filename='color_hsl.json') -def test_color3_hsl(input): - return parse_color3(input) +@json_test() +def test_color_hexadecimal_4(input): + if not (color := parse_color4(input)): + return None + assert color.space == 'srgb' + (*coordinates, alpha) = color + result = f'rgb{"a" if alpha != 1 else ""}(' + result += f'{", ".join(_number(coordinate * 255) for coordinate in coordinates)}' + if alpha != 1: + result += f', {_number(alpha)}' + result += ')' + return result + + +@json_test(filename='color_hexadecimal_3.json') +def test_color_hexadecimal_3_with_4(input): + if not (color := parse_color4(input)): + return None + assert color.space == 'srgb' + (*coordinates, alpha) = color + result = f'rgb{"a" if alpha != 1 else ""}(' + result += f'{", ".join(_number(coordinate * 255) for coordinate in coordinates)}' + if alpha != 1: + result += f', {_number(alpha)}' + result += ')' + return result -@json_test(filename='color_hsl.json') -def test_color4_hsl(input): - color = parse_color4(input) +@json_test() +def test_color_hsl_3(input): + if not (color := parse_color3(input)): + return None + (*coordinates, alpha) = color + result = f'rgb{"a" if alpha != 1 else ""}(' + result += f'{", ".join(_number(coordinate * 255) for coordinate in coordinates)}' + if alpha != 1: + result += f', {_number(alpha)}' + result += ')' + return result + + +@json_test(filename='color_hsl_3.json') +def test_color_hsl_3_with_4(input): + if not (color := parse_color4(input)): + return None assert color.space == 'hsl' - rgb = hls_to_rgb(color[0] / 360, color[2] / 100, color[1] / 100) - return RGBA(*rgb, color.alpha) if (color and color != 'currentColor') else color + (*coordinates, alpha) = color.to('srgb') + result = f'rgb{"a" if alpha != 1 else ""}(' + result += f'{", ".join(_number(coordinate * 255) for coordinate in coordinates)}' + if alpha != 1: + result += f', {_number(alpha)}' + result += ')' + return result @json_test() -def test_color4_hwb(input): - color = parse_color4(input) +def test_color_hsl_4(input): + if not (color := parse_color4(input)): + return None + assert color.space == 'hsl' + (*coordinates, alpha) = color.to('srgb') + result = f'rgb{"a" if alpha != 1 else ""}(' + result += f'{", ".join(_number(coordinate * 255) for coordinate in coordinates)}' + if alpha != 1: + result += f', {_number(alpha)}' + result += ')' + return result + + +@json_test() +def test_color_hwb_4(input): + if not (color := parse_color4(input)): + return None assert color.space == 'hwb' - white, black = color[1:3] - if white + black >= 100: - rgb = (255 * white / (white + black),) * 3 - else: - rgb = hls_to_rgb(color[0] / 360, 0.5, 1) - rgb = (2.55 * ((channel * (100 - white - black)) + white) for channel in rgb) - rgb = (round(coordinate + 0.001) for coordinate in rgb) - coordinates = ', '.join( - str(int(coordinate) if coordinate.is_integer() else coordinate) - for coordinate in rgb) - if color.alpha == 0: - return f'rgba({coordinates}, 0)' - elif color.alpha == 1: - return f'rgb({coordinates})' - else: - return f'rgba({coordinates}, {color.alpha})' - return RGBA(*rgb, color.alpha) if (color and color != 'currentColor') else color + (*coordinates, alpha) = color.to('srgb') + result = f'rgb{"a" if alpha != 1 else ""}(' + result += f'{", ".join(_number(coordinate * 255) for coordinate in coordinates)}' + if alpha != 1: + result += f', {_number(alpha)}' + result += ')' + return result @json_test() -def test_color4_color_function(input): - color = parse_color4(input) - coordinates = ' '.join( - str(int(coordinate) if coordinate.is_integer() else round(coordinate, 3)) - for coordinate in color.coordinates) - if color.alpha == 0: - return f'color({color.space} {coordinates} / 0)' - elif color.alpha == 1: - return f'color({color.space} {coordinates})' - else: - return f'color({color.space} {coordinates} / {color.alpha})' +def test_color_keywords_3(input): + if not (color := parse_color3(input)): + return None + elif isinstance(color, str): + return color + (*coordinates, alpha) = color + result = f'rgb{"a" if alpha != 1 else ""}(' + result += f'{", ".join(_number(coordinate * 255) for coordinate in coordinates)}' + if alpha != 1: + result += f', {_number(alpha)}' + result += ')' + return result + + +@json_test(filename='color_keywords_3.json') +def test_color_keywords_3_with_4(input): + if not (color := parse_color4(input)): + return None + elif isinstance(color, str): + return color + assert color.space == 'srgb' + (*coordinates, alpha) = color + result = f'rgb{"a" if alpha != 1 else ""}(' + result += f'{", ".join(_number(coordinate * 255) for coordinate in coordinates)}' + if alpha != 1: + result += f', {_number(alpha)}' + result += ')' + return result @json_test() -def test_color4_lab_lch_oklab_oklch(input): - color = parse_color4(input) - coordinates = ' '.join( - str(int(coordinate) if coordinate.is_integer() else round(coordinate, 3)) - for coordinate in color.coordinates) - if color.alpha == 0: - return f'{color.space}({coordinates} / 0)' - elif color.alpha == 1: - return f'{color.space}({coordinates})' - else: - return f'{color.space}({coordinates} / {color.alpha})' - - -@pytest.mark.parametrize(('filename', 'parse_color'), ( - ('color_keywords.json', parse_color3), - ('color_keywords.json', parse_color4), - ('color3_keywords.json', parse_color3), - ('color4_keywords.json', parse_color4), -)) -def test_color_keywords(filename, parse_color): - for css, expected in load_json(filename): - result = parse_color(css) - if result is not None: - r, g, b, a = result - result = [r * 255, g * 255, b * 255, a] - assert result == expected +def test_color_keywords_4(input): + if not (color := parse_color4(input)): + return None + elif isinstance(color, str): + return color + assert color.space == 'srgb' + (*coordinates, alpha) = color + result = f'rgb{"a" if alpha != 1 else ""}(' + result += f'{", ".join(_number(coordinate * 255) for coordinate in coordinates)}' + if alpha != 1: + result += f', {_number(alpha)}' + result += ')' + return result + + +@json_test() +def test_color_lab_4(input): + if not (color := parse_color4(input)): + return None + elif isinstance(color, str): + return color + assert color.space == 'lab' + (*coordinates, alpha) = color + result = f'{color.space}(' + result += f'{" ".join(_number(coordinate) for coordinate in coordinates)}' + if alpha != 1: + result += f' / {_number(alpha)}' + result += ')' + return result + + +@json_test() +def test_color_oklab_4(input): + if not (color := parse_color4(input)): + return None + elif isinstance(color, str): + return color + assert color.space == 'oklab' + (*coordinates, alpha) = color + result = f'{color.space}(' + result += f'{" ".join(_number(coordinate) for coordinate in coordinates)}' + if alpha != 1: + result += f' / {_number(alpha)}' + result += ')' + return result + + +@json_test() +def test_color_lch_4(input): + if not (color := parse_color4(input)): + return None + elif isinstance(color, str): + return color + assert color.space == 'lch' + (*coordinates, alpha) = color + result = f'{color.space}(' + result += f'{" ".join(_number(coordinate) for coordinate in coordinates)}' + if alpha != 1: + result += f' / {_number(alpha)}' + result += ')' + return result + + +@json_test() +def test_color_oklch_4(input): + if not (color := parse_color4(input)): + return None + elif isinstance(color, str): + return color + assert color.space == 'oklch' + (*coordinates, alpha) = color + result = f'{color.space}(' + result += f'{" ".join(_number(coordinate) for coordinate in coordinates)}' + if alpha != 1: + result += f' / {_number(alpha)}' + result += ')' + return result @json_test() diff --git a/tinycss2/color3.py b/tinycss2/color3.py index 048d859..238c40a 100644 --- a/tinycss2/color3.py +++ b/tinycss2/color3.py @@ -113,14 +113,14 @@ def _parse_rgb(args, alpha): def _parse_hsl(args, alpha): """Parse a list of HSL channels. - If args is a list of 1 INTEGER token and 2 PERCENTAGE tokens, return RGB + If args is a list of 1 NUMBER token and 2 PERCENTAGE tokens, return RGB values as a tuple of 3 floats in 0..1. Otherwise, return None. """ types = [arg.type for arg in args] - if types == ['number', 'percentage', 'percentage'] and args[0].is_integer: + if types == ['number', 'percentage', 'percentage']: r, g, b = hls_to_rgb( - args[0].int_value / 360, args[2].value / 100, args[1].value / 100) + args[0].value / 360, args[2].value / 100, args[1].value / 100) return RGBA(r, g, b, alpha) diff --git a/tinycss2/color4.py b/tinycss2/color4.py index df1b455..6296760 100644 --- a/tinycss2/color4.py +++ b/tinycss2/color4.py @@ -184,7 +184,7 @@ def parse_color(input): :returns: * :obj:`None` if the input is not a valid color value. (No exception is raised.) - * The string ``'currentColor'`` for the ``currentColor`` keyword + * The string ``'currentcolor'`` for the ``currentcolor`` keyword * A :class:`Color` object for every other values, including keywords. """ @@ -194,7 +194,7 @@ def parse_color(input): token = input if token.type == 'ident': if token.lower_value == 'currentcolor': - return 'currentColor' + return 'currentcolor' elif token.lower_value == 'transparent': return Color('srgb', (0, 0, 0), 0) elif color := _COLOR_KEYWORDS.get(token.lower_value): @@ -273,7 +273,7 @@ def _parse_rgb(args, alpha): Input R, G, B ranges are [0, 255], output are [0, 1]. """ - if _types(args) not in ({'number'}, {'percentage'}): + if _types(args) not in (set(), {'number'}, {'percentage'}): return coordinates = [ arg.value / 255 if arg.type == 'number' else @@ -291,7 +291,7 @@ def _parse_hsl(args, alpha): H range is [0, 360). S, L ranges are [0, 100]. """ - if _types(args[1:]) not in ({'number'}, {'percentage'}): + if _types(args[1:]) not in (set(), {'number'}, {'percentage'}): return if (hue := _parse_hue(args[0])) is None: return @@ -306,13 +306,13 @@ def _parse_hsl(args, alpha): def _parse_hwb(args, alpha): """Parse a list of HWB channels. - If args is a list of 1 NUMBER or ANGLE token and 2 PERCENTAGE tokens, - return HWB :class:`Color`. Otherwise, return None. + If args is a list of 1 NUMBER or ANGLE token and 2 NUMBER or PERCENTAGE + tokens, return HWB :class:`Color`. Otherwise, return None. H range is [0, 360). W, B ranges are [0, 100]. """ - if _types(args[1:]) > {'percentage'}: + if not _types(args[1:]) <= {'number', 'percentage'}: return if (hue := _parse_hue(args[0])) is None: return @@ -333,7 +333,7 @@ def _parse_lab(args, alpha): L range is [0, 100]. a, b ranges are [-125, 125]. """ - if _types(args) > {'number', 'percentage'}: + if not _types(args) <= {'number', 'percentage'}: return coordinates = [ None if args[0].type == 'ident' else args[0].value, @@ -354,7 +354,7 @@ def _parse_lch(args, alpha): L range is [0, 100]. C range is [0, 150]. H ranges is [0, 360). """ - if _types(args[:2]) > {'number', 'percentage'}: + if not _types(args[:2]) <= {'number', 'percentage'}: return if (hue := _parse_hue(args[2])) is None: return @@ -376,7 +376,7 @@ def _parse_oklab(args, alpha): L range is [0, 100]. a, b ranges are [-0.4, 0.4]. """ - if _types(args) > {'number', 'percentage'}: + if not _types(args) <= {'number', 'percentage'}: return coordinates = [ None if args[0].type == 'ident' else ( @@ -398,7 +398,7 @@ def _parse_oklch(args, alpha): L range is [0, 1]. C range is [0, 0.4]. H range is [0, 360). """ - if _types(args[:2]) > {'number', 'percentage'}: + if not _types(args[:2]) <= {'number', 'percentage'}: return if (hue := _parse_hue(args[2])) is None: return @@ -418,6 +418,8 @@ def _parse_color(space, args, alpha): Ranges are [0, 1]. """ + if not _types(args) <= {'number', 'percentage'}: + return if space.type != 'ident' or (space := space.lower_value) not in _FUNCTION_SPACES: return if space == 'xyz':