diff --git a/colosseum/constants.py b/colosseum/constants.py index 431c9b6ae..e5a45e5fc 100644 --- a/colosseum/constants.py +++ b/colosseum/constants.py @@ -1,6 +1,6 @@ from .validators import (ValidationError, is_border_spacing, is_color, - is_integer, is_length, is_number, is_percentage, - is_quote, is_rect) + is_cursor, is_integer, is_length, is_number, + is_percentage, is_quote, is_rect) class Choices: @@ -509,6 +509,30 @@ def value(self, context): # 18.1 Cursors # cursor +AUTO = 'auto' +CROSSHAIR = 'crosshair' +DEFAULT = 'default' +POINTER = 'pointer' +MOVE = 'move' +E_RESIZE = 'e-resize' +NE_RESIZE = 'ne-resize' +NW_RESIZE = 'nw-resize' +N_RESIZE = 'n-resize' +SE_RESIZE = 'se-resize' +SW_RESIZE = 'sw-resize' +S_RESIZE = 's-resize' +W_RESIZE = 'w-resize' +TEXT = 'text' +WAIT = 'wait' +PROGRESS = 'progress' +HELP = 'help' + +CURSOR_OPTIONS = [AUTO, CROSSHAIR, DEFAULT, POINTER, MOVE, E_RESIZE, NE_RESIZE, NW_RESIZE, N_RESIZE, SE_RESIZE, + SW_RESIZE, S_RESIZE, W_RESIZE, TEXT, WAIT, PROGRESS, HELP] + +# Since the order is important, the cursor options are used in the is_cursor validator +CURSOR_CHOICES = Choices(validators=[is_cursor], explicit_defaulting_constants=[INHERIT]) + ###################################################################### # 18.4 Dynamic outlines ###################################################################### diff --git a/colosseum/declaration.py b/colosseum/declaration.py index 4b2515373..40320cbb8 100644 --- a/colosseum/declaration.py +++ b/colosseum/declaration.py @@ -21,7 +21,7 @@ TEXT_TRANSFORM_CHOICES, TOP, TRANSPARENT, UNICODE_BIDI_CHOICES, VISIBILITY_CHOICES, VISIBLE, WHITE_SPACE_CHOICES, WIDOWS_CHOICES, WORD_SPACING_CHOICES, Z_INDEX_CHOICES, OtherProperty, - TextAlignInitialValue, default, + TextAlignInitialValue, default, CURSOR_CHOICES, ) from .exceptions import ValidationError from .wrappers import Border, BorderBottom, BorderLeft, BorderRight, BorderTop, Outline @@ -410,7 +410,7 @@ def __init__(self, **style): # 18. User interface ################################################# # 18.1 Cursors - # cursor + cursor = validated_property('cursor', CURSOR_CHOICES, initial=AUTO) # 18.4 Dynamic outlines outline_width = validated_property('outline_width', choices=OUTLINE_WIDTH_CHOICES, initial=MEDIUM) diff --git a/colosseum/parser.py b/colosseum/parser.py index a2b734a6c..85b7acb8a 100644 --- a/colosseum/parser.py +++ b/colosseum/parser.py @@ -5,7 +5,7 @@ from .exceptions import ValidationError from .shapes import Rect from .units import Unit, px -from .wrappers import BorderSpacing, Quotes +from .wrappers import BorderSpacing, Cursor, Quotes, Uri def units(value): @@ -369,3 +369,79 @@ def border_bottom(value): def border_top(value): """Parse border string into a dictionary of outline properties.""" return border(value, direction='top') + + +############################################################################## +# Uri +############################################################################## +def uri(value): + """Parse a url from a value. + + Accepts: + * url("") + * url( "" ) + * url('') + * url( '' ) + * url() + * url( ) + """ + if isinstance(value, str): + value = value.strip() + else: + raise ValueError('Value {value} must be a string') + + if value.startswith('url(') and value.endswith(')'): + # Remove the 'url(' and ')' + value = value[4:-1].strip() + + # Single quotes and optional spaces + if value.startswith("'") and value.endswith("'"): + return Uri(value[1:-1]) + # Double quotes and optional spaces + elif value.startswith('"') and value.endswith('"'): + # '("some.url")' + return Uri(value[1:-1]) + # No quotes and optional spaces + else: + return Uri(value) + + raise ValueError('Invalid url %s' % value) + + +############################################################################## +# Cursor +############################################################################## +def cursor(values): + """Parse a cursor from a value.""" + from .constants import CURSOR_OPTIONS + + if isinstance(values, str): + values = [val.strip() for val in values.split(',')] + + validated_values = [] + has_cursor_option = False + option_count = 0 + for value in values: + if value in CURSOR_OPTIONS: + has_cursor_option = True + validated_values.append(value) + option_count += 1 + + if option_count > 1: + raise ValueError('There can only be one cursor option in {values}!'.format(values=values)) + + continue + else: + if has_cursor_option: + raise ValueError('Values {values} are in incorrect order. ' + 'Cursor option must come last!'.format(values=values)) + try: + value = uri(value) + validated_values.append(value) + continue + except ValueError: + raise ValueError('Value {value} is not a valid url value'.format(value=value)) + + raise ValueError('Value {value} is not a valid cursor value'.format(value=value)) + + return Cursor(validated_values) diff --git a/colosseum/validators.py b/colosseum/validators.py index 4ed0aebc2..7ba8ec978 100644 --- a/colosseum/validators.py +++ b/colosseum/validators.py @@ -2,8 +2,9 @@ Validate values of different css properties. """ -from . import parser -from . import units +import re + +from . import parser, units from .exceptions import ValidationError @@ -147,3 +148,44 @@ def is_quote(value): is_quote.description = '[ ]+' + + +URI_RE = re.compile(r"""( + (?:url\(\s?'[A-Za-z0-9\./\:\?]*'\s?\)) # Single quotes and optional spaces + | + (?:url\(\s?"[A-Za-z0-9\./\:\?]*"\s?\)) # Double quotes and optional spaces + | + (?:url\(\s?[A-Za-z0-9\./\:\?]*\s?\)) # No quotes and optional spaces +)""", re.VERBOSE) + + +def is_uri(value): + """Validate value is .""" + try: + value = parser.uri(value) + except ValueError as error: + raise ValidationError(str(error)) + + return value + + +is_uri.description = '' + + +def is_cursor(value): + """ + Validate if values are correct cursor values and in correct order and quantity. + + This validator returns a list. + """ + try: + value = parser.cursor(value) + except ValueError as error: + raise ValidationError(str(error)) + + return value + + +is_cursor.description = ('[ [ ,]* [ auto | crosshair | default | pointer | move | e-resize ' + '| ne-resize | nw-resize | n-resize | se-resize | sw-resize | s-resize ' + '| w-resize | text | wait | help | progress ] ]') diff --git a/colosseum/wrappers.py b/colosseum/wrappers.py index c1688cb09..05809ff1c 100644 --- a/colosseum/wrappers.py +++ b/colosseum/wrappers.py @@ -151,3 +151,87 @@ class BorderLeft(Shorthand): class Border(Shorthand): VALID_KEYS = ['border_width', 'border_style', 'border_color'] + + +class Uri: + """Wrapper for a url.""" + + def __init__(self, url): + self._url = url + + def __repr__(self): + return 'url("%s")' % self._url + + def __str__(self): + return repr(self) + + @property + def url(self): + return self._url + + +class ImmutableList(list): + """Immutable list to store list properties.""" + + def __init__(self, iterable=()): + super().__init__(iterable) + + def _get_error_message(self, err): + return str(err).replace('list', self.__class__.__name__, 1) + + # def __eq__(self, other): + # return other.__class__ == self.__class__ and self == other + + def __getitem__(self, index): + try: + return super().__getitem__(index) + except Exception as err: + error_msg = self._get_error_message(err) + raise err.__class__(error_msg) + + def __setitem__(self, index, value): + raise TypeError("{} values cannot be changed!".format(self.__class__.__name__)) + + def __hash__(self): + return hash((self.__class__.__name__, tuple(self))) + + def __repr__(self): + class_name = self.__class__.__name__ + if len(self) != 0: + text = '{class_name}([{data}])'.format(data=repr(list(self))[1:-1], class_name=class_name) + else: + text = '{class_name}()'.format(class_name=class_name) + + return text + + def __str__(self): + return ', '.join(str(v) for v in self) + + def copy(self): + return self.__class__(self) + + # Disable mutating methods + def append(self, object): + raise TypeError("{} values cannot be changed!".format(self.__class__.__name__)) + + def extend(self, iterable): + raise TypeError("{} values cannot be changed!".format(self.__class__.__name__)) + + def insert(self, index, object): + raise TypeError("{} values cannot be changed!".format(self.__class__.__name__)) + + def pop(self, index=None): + raise TypeError("{} values cannot be changed!".format(self.__class__.__name__)) + + def remove(self, value): + raise TypeError("{} values cannot be changed!".format(self.__class__.__name__)) + + def reverse(self): + raise TypeError("{} values cannot be changed!".format(self.__class__.__name__)) + + def sort(self, cmp=None, key=None, reverse=False): + raise TypeError("{} values cannot be changed!".format(self.__class__.__name__)) + + +class Cursor(ImmutableList): + """Immutable list to store cursor property.""" diff --git a/tests/test_declaration.py b/tests/test_declaration.py index 127447428..d018b3ce3 100644 --- a/tests/test_declaration.py +++ b/tests/test_declaration.py @@ -8,7 +8,7 @@ from colosseum.declaration import CSS, validated_property from colosseum.units import percent, px from colosseum.validators import (is_color, is_integer, is_length, is_number, - is_percentage) + is_percentage, is_uri) from colosseum.wrappers import BorderSpacing, Quotes from .utils import TestNode @@ -186,6 +186,38 @@ class MyObject: "Invalid value 'invalid' for CSS property 'prop'; Valid values are: " ) + def test_allow_uri(self): + class MyObject: + prop = validated_property('prop', choices=Choices(validators=[is_uri]), initial='url(google.com)') + + obj = MyObject() + self.assertEqual(str(obj.prop), 'url("google.com")') + + with self.assertRaises(ValueError): + obj.prop = 10 + with self.assertRaises(ValueError): + obj.prop = 20 * px + with self.assertRaises(ValueError): + obj.prop = 30 * percent + with self.assertRaises(ValueError): + obj.prop = 'a' + with self.assertRaises(ValueError): + obj.prop = 'b' + with self.assertRaises(ValueError): + obj.prop = None + with self.assertRaises(ValueError): + obj.prop = 'none' + + # Check the error message + try: + obj.prop = 'invalid' + self.fail('Should raise ValueError') + except ValueError as v: + self.assertEqual( + str(v), + "Invalid value 'invalid' for CSS property 'prop'; Valid values are: " + ) + def test_values(self): class MyObject: prop = validated_property('prop', choices=Choices('a', 'b', None), initial='a') @@ -742,6 +774,51 @@ def test_directional_property(self): self.assertEqual(node.style.margin_left, 0) self.assertTrue(node.style.dirty) + def test_validated_property_cursor_default_value(self): + node = TestNode(style=CSS()) + node.layout.dirty = None + + self.assertEqual(str(node.style.cursor), AUTO) + + def test_validated_property_cursor_set_valid_values(self): + node = TestNode(style=CSS()) + node.layout.dirty = None + + node.style.cursor = 'url(test), auto' + self.assertEqual(str(node.style.cursor), 'url("test"), auto') + + node.style.cursor = "url('test')", AUTO + self.assertEqual(str(node.style.cursor), 'url("test"), auto') + + node.style.cursor = ["url('test')", AUTO] + self.assertEqual(str(node.style.cursor), 'url("test"), auto') + + node.style.cursor = ["url('test')", "url('test2')", AUTO] + self.assertEqual(str(node.style.cursor), 'url("test"), url("test2"), auto') + + def test_validated_property_cursor_set_invalid_str_values(self): + node = TestNode(style=CSS()) + node.layout.dirty = None + + with self.assertRaises(ValueError): + node.style.cursor = 'boom' + + with self.assertRaises(ValueError): + node.style.cursor = 'auto, url(google.com)' + + with self.assertRaises(ValueError): + node.style.cursor = 'auto url(google.com)' + + def test_validated_property_cursor_set_invalid_list_values(self): + node = TestNode(style=CSS()) + node.layout.dirty = None + + with self.assertRaises(ValueError): + node.style.cursor = ['boom'] + + with self.assertRaises(ValueError): + node.style.cursor = [AUTO, 'url(google.com)'] + def test_set_multiple_properties(self): node = TestNode(style=CSS()) node.layout.dirty = None @@ -1149,10 +1226,14 @@ def test_str(self): height=20, margin=(30, 40, 50, 60), display=BLOCK, + cursor=['url(some.cursor.uri)', AUTO] ) + print(str(node.style)) + self.assertEqual( str(node.style), + 'cursor: url("some.cursor.uri"), auto; ' "display: block; " "height: 20px; " "margin-bottom: 50px; " diff --git a/tests/test_parser.py b/tests/test_parser.py index 145c5c49e..941e4e511 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -3,11 +3,13 @@ from colosseum import parser from colosseum.colors import hsl, rgb +from colosseum.constants import CURSOR_OPTIONS from colosseum.parser import (border, border_bottom, border_left, border_right, border_top, color, outline) from colosseum.shapes import Rect from colosseum.units import (ch, cm, em, ex, inch, mm, pc, percent, pt, px, vh, vmax, vmin, vw) +from colosseum.wrappers import Cursor class ParseUnitTests(TestCase): @@ -679,3 +681,231 @@ def test_parse_border_shorthand_invalid_too_many_items(self): with self.assertRaises(ValueError): func('black solid thick black thick') + + +class ParseUriTests(TestCase): + + def test_url_valid_single_quotes_url(self): + url = parser.uri("url('some.url')") + self.assertEqual(str(url), 'url("some.url")') + + def test_url_valid_single_quotes_spaces_url_left(self): + url = parser.uri("url( 'some.url')") + self.assertEqual(str(url), 'url("some.url")') + + def test_url_valid_single_quotes_spaces_url_right(self): + url = parser.uri("url('some.url' )") + self.assertEqual(str(url), 'url("some.url")') + + def test_url_valid_single_quotes_spaces_url_left_right(self): + url = parser.uri("url( 'some.url' )") + self.assertEqual(str(url), 'url("some.url")') + + def test_url_valid_double_quotes_url(self): + url = parser.uri('url("some.url")') + self.assertEqual(str(url), 'url("some.url")') + + def test_url_valid_double_quotes_spaces_url_left(self): + url = parser.uri('url( "some.url")') + self.assertEqual(str(url), 'url("some.url")') + + def test_url_valid_double_quotes_spaces_url_right(self): + url = parser.uri('url("some.url" )') + self.assertEqual(str(url), 'url("some.url")') + + def test_url_valid_double_quotes_spaces_url_left_right(self): + url = parser.uri('url( "some.url" )') + self.assertEqual(str(url), 'url("some.url")') + + def test_url_valid_no_quotes_spaces_url_left_right(self): + url = parser.uri('url( some.url )') + self.assertEqual(str(url), 'url("some.url")') + + def test_url_valid_no_quotes_spaces_url_left(self): + url = parser.uri('url( some.url)') + self.assertEqual(str(url), 'url("some.url")') + + def test_url_valid_no_quotes_spaces_url_right(self): + url = parser.uri('url(some.url )') + self.assertEqual(str(url), 'url("some.url")') + + def test_url_valid_no_quotes_url(self): + url = parser.uri('url(some.url)') + self.assertEqual(str(url), 'url("some.url")') + + def test_url_valid_no_quotes_escaped_chars(self): + url = parser.uri(r'url(some.\(url)') + self.assertEqual(str(url), r'url("some.\(url")') + + url = parser.uri(r'url(some.\)url)') + self.assertEqual(str(url), r'url("some.\)url")') + + url = parser.uri(r'url(some.\ url)') + self.assertEqual(str(url), r'url("some.\ url")') + + url = parser.uri(r"url(some.\"url)") + self.assertEqual(str(url), r'url("some.\"url")') + + url = parser.uri(r"url(some.\'url)") + self.assertEqual(str(url), r'url("some.\'url")') + + +class ParseCursorTests(TestCase): + + def test_cursor_valid_string_1_item_option(self): + for option in CURSOR_OPTIONS: + cursor = parser.cursor(option) + self.assertIsInstance(cursor, Cursor) + self.assertEqual(cursor, Cursor([option])) + + def test_cursor_valid_string_1_item_uri_1(self): + cursor = parser.cursor('url("some.uri")') + self.assertIsInstance(cursor, Cursor) + self.assertEqual(str(cursor[0]), 'url("some.uri")') + + def test_cursor_valid_string_2_items_uri_2(self): + cursor = parser.cursor("url(some.uri), url(some.uri2)") + self.assertIsInstance(cursor, Cursor) + self.assertEqual(str(cursor[0]), 'url("some.uri")') + self.assertEqual(str(cursor[1]), 'url("some.uri2")') + + def test_cursor_valid_string_2_items_uri_1_option_1(self): + for option in CURSOR_OPTIONS: + cursor = parser.cursor("url(some.uri), {option}".format(option=option)) + self.assertIsInstance(cursor, Cursor) + self.assertEqual(str(cursor[0]), 'url("some.uri")') + self.assertEqual(str(cursor[1]), option) + + def test_cursor_valid_string_3_items_uri_2_option_1(self): + for option in CURSOR_OPTIONS: + cursor = parser.cursor("url(some.uri), url(some.uri2), {option}".format(option=option)) + self.assertIsInstance(cursor, Cursor) + self.assertEqual(str(cursor[0]), 'url("some.uri")') + self.assertEqual(str(cursor[1]), 'url("some.uri2")') + self.assertEqual(str(cursor[2]), option) + + def test_cursor_valid_list_1_item_option(self): + for option in CURSOR_OPTIONS: + cursor = parser.cursor([option]) + self.assertIsInstance(cursor, Cursor) + self.assertEqual(cursor, Cursor([option])) + + def test_cursor_valid_list_1_item_uri_1(self): + cursor = parser.cursor(["url(some.uri)"]) + self.assertIsInstance(cursor, Cursor) + self.assertEqual(str(cursor[0]), 'url("some.uri")') + + def test_cursor_valid_list_2_items_uri_2(self): + cursor = parser.cursor(["url(some.uri)", "url(some.uri2)"]) + self.assertIsInstance(cursor, Cursor) + self.assertEqual(str(cursor[0]), 'url("some.uri")') + self.assertEqual(str(cursor[1]), 'url("some.uri2")') + + def test_cursor_valid_list_2_items_uri_1_option_1(self): + for option in CURSOR_OPTIONS: + cursor = parser.cursor(["url(some.uri)", option]) + self.assertIsInstance(cursor, Cursor) + self.assertEqual(str(cursor[0]), 'url("some.uri")') + self.assertEqual(str(cursor[1]), option) + + def test_cursor_valid_list_3_items_uri_2_option_1(self): + for option in CURSOR_OPTIONS: + cursor = parser.cursor(["url(some.uri)", "url(some.uri2)", option]) + self.assertIsInstance(cursor, Cursor) + self.assertEqual(str(cursor[0]), 'url("some.uri")') + self.assertEqual(str(cursor[1]), 'url("some.uri2")') + self.assertEqual(str(cursor[2]), option) + + # Invalid cases + def test_cursor_invalid_order_string_1_item_invalid_value(self): + with self.assertRaises(ValueError): + parser.cursor("foobar") + + def test_cursor_invalid_order_string_2_items_uri_1_option_1_invalid_uri(self): + with self.assertRaises(ValueError): + parser.cursor("auto, url( some.uri )") + + def test_cursor_invalid_order_string_2_items_uri_1_option_1_invalid_option(self): + with self.assertRaises(ValueError): + parser.cursor("foobar, url(some.uri)") + + def test_cursor_invalid_order_string_2_items_uri_1_option_1(self): + for option in CURSOR_OPTIONS: + with self.assertRaises(ValueError): + parser.cursor("{option}, url(some.uri)".format(option=option)) + + def test_cursor_invalid_order_string_3_items_uri_2_option_1(self): + for option in CURSOR_OPTIONS: + with self.assertRaises(ValueError): + parser.cursor("url(some.uri), {option}, url(some.uri)".format(option=option)) + + with self.assertRaises(ValueError): + parser.cursor("{option}, url(some.uri), url(some.uri)".format(option=option)) + + def test_cursor_invalid_order_string_2_items_option_2(self): + perms = permutations(CURSOR_OPTIONS, 2) + for (option1, option2) in perms: + with self.assertRaises(ValueError): + parser.cursor("{option1}, {option2}".format(option1=option1, option2=option2)) + + def test_cursor_invalid_order_string_3_items_option_3(self): + perms = permutations(CURSOR_OPTIONS, 3) + for (option1, option2, option3) in perms: + with self.assertRaises(ValueError): + parser.cursor("{option1}, {option2}, {option3}".format(option1=option1, option2=option2, + option3=option3)) + + def test_cursor_invalid_order_string_3_items_option_2_uri_1(self): + perms = permutations(CURSOR_OPTIONS, 2) + for (option1, option2) in perms: + with self.assertRaises(ValueError): + parser.cursor("{option1}, {option2}, url(some.url)".format(option1=option1, option2=option2)) + + with self.assertRaises(ValueError): + parser.cursor("{option1}, url(some.url), {option2}".format(option1=option1, option2=option2)) + + def test_cursor_invalid_order_list_1_item_invalid_value(self): + with self.assertRaises(ValueError): + parser.cursor(["foobar"]) + + def test_cursor_invalid_order_list_2_items_uri_1_option_1_invalid_uri(self): + with self.assertRaises(ValueError): + parser.cursor(["auto", "url( some.uri )"]) + + def test_cursor_invalid_order_list_2_items_uri_1_option_1_invalid_option(self): + with self.assertRaises(ValueError): + parser.cursor(["foobar", "url(some.uri)"]) + + def test_cursor_invalid_order_list_2_items_uri_1_option_1(self): + for option in CURSOR_OPTIONS: + with self.assertRaises(ValueError): + parser.cursor([option, "url(some.uri)"]) + + def test_cursor_invalid_order_list_3_items_uri_2_option_1(self): + for option in CURSOR_OPTIONS: + with self.assertRaises(ValueError): + parser.cursor(["url(some.uri)", option, "url(some.uri)"]) + + with self.assertRaises(ValueError): + parser.cursor([option, "url(some.uri)", "url(some.uri)"]) + + def test_cursor_invalid_order_list_2_items_option_2(self): + perms = permutations(CURSOR_OPTIONS, 2) + for (option1, option2) in perms: + with self.assertRaises(ValueError): + parser.cursor([option1, option2]) + + def test_cursor_invalid_order_list_3_items_option_3(self): + perms = permutations(CURSOR_OPTIONS, 3) + for (option1, option2, option3) in perms: + with self.assertRaises(ValueError): + parser.cursor([option1, option2, option3]) + + def test_cursor_invalid_order_list_3_items_option_2_uri_1(self): + perms = permutations(CURSOR_OPTIONS, 2) + for (option1, option2) in perms: + with self.assertRaises(ValueError): + parser.cursor([option1, option2, "url(some.url)"]) + + with self.assertRaises(ValueError): + parser.cursor([option1, "url(some.url)", option2]) diff --git a/tests/test_validators.py b/tests/test_validators.py index 19f5bceb9..1d7e5322c 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -3,10 +3,10 @@ from colosseum.shapes import Rect from colosseum.units import px from colosseum.validators import (ValidationError, is_border_spacing, - is_integer, is_number, is_quote, is_rect) + is_cursor, is_integer, is_number, is_quote, + is_rect, is_uri) from colosseum.wrappers import Quotes - class NumericTests(TestCase): def test_integer(self): @@ -164,3 +164,88 @@ def test_quote_valid(self): def test_quote_invalid(self): with self.assertRaises(ValidationError): is_quote("'<' '>' '{'") + + +class UriTests(TestCase): + """Comprehensive tests are found on test_parser.py.""" + + def test_url_valid(self): + url = is_uri("url(some.url)") + self.assertEqual(str(url), 'url("some.url")') + + url = is_uri(" url(some.url) ") + self.assertEqual(str(url), 'url("some.url")') + + url = is_uri(r"url(some.\ url)") + self.assertEqual(str(url), r'url("some.\ url")') + + url = is_uri("url('some.url')") + self.assertEqual(str(url), 'url("some.url")') + + url = is_uri("url( 'some.url' )") + self.assertEqual(str(url), 'url("some.url")') + + url = is_uri('url("some.url")') + self.assertEqual(str(url), 'url("some.url")') + + url = is_uri('url( "some.url" )') + self.assertEqual(str(url), 'url("some.url")') + + +class CursorTests(TestCase): + """Comprehensive tests are found on test_parser.py.""" + + def test_cursor_valid_1_item(self): + cursor = is_cursor("url(some.url)") + self.assertEqual(str(cursor), 'url("some.url")') + + cursor = is_cursor(" url(some.url) ") + self.assertEqual(str(cursor), 'url("some.url")') + + cursor = is_cursor("url('some.url')") + self.assertEqual(str(cursor), 'url("some.url")') + + cursor = is_cursor("url( 'some.url' )") + self.assertEqual(str(cursor), 'url("some.url")') + + cursor = is_cursor('url("some.url")') + self.assertEqual(str(cursor), 'url("some.url")') + + cursor = is_cursor('url( "some.url" )') + self.assertEqual(str(cursor), 'url("some.url")') + + def test_cursor_valid_2_items(self): + cursor = is_cursor("url(some.url), url(some.url2)") + self.assertEqual(str(cursor), 'url("some.url"), url("some.url2")') + + cursor = is_cursor("url(some.url), auto") + self.assertEqual(str(cursor), 'url("some.url"), auto') + + cursor = is_cursor(["url(some.url)", "url(some.url2)"]) + self.assertEqual(str(cursor), 'url("some.url"), url("some.url2")') + + cursor = is_cursor(["url(some.url)", "auto"]) + self.assertEqual(str(cursor), 'url("some.url"), auto') + + def test_cursor_invalid_1_item(self): + with self.assertRaises(ValidationError): + is_cursor("foobar") + + with self.assertRaises(ValidationError): + is_cursor(["foobar"]) + + def test_cursor_invalid_2_items(self): + with self.assertRaises(ValidationError): + is_cursor("foobar, blah") + + with self.assertRaises(ValidationError): + is_cursor("auto, url( something )") + + with self.assertRaises(ValidationError): + is_cursor(["foobar", 'blah']) + + with self.assertRaises(ValidationError): + is_cursor(["auto", "url(something)"]) + + with self.assertRaises(ValidationError): + is_cursor(["url(something)", "auto", "url(something)"]) diff --git a/tests/test_wrappers.py b/tests/test_wrappers.py index fb6ab3cca..f7a4d5fae 100644 --- a/tests/test_wrappers.py +++ b/tests/test_wrappers.py @@ -4,8 +4,8 @@ from colosseum.units import px from colosseum.wrappers import (Border, BorderBottom, BorderLeft, BorderRight, - BorderSpacing, BorderTop, Outline, Quotes, - Shorthand) + BorderSpacing, BorderTop, Cursor, + ImmutableList, Outline, Quotes, Shorthand) class BorderSpacingTests(TestCase): @@ -328,3 +328,92 @@ def test_shorthand_outline_invalid_kwargs(self): for wrapper_class in [Border, BorderBottom, BorderLeft, BorderRight, BorderTop]: with self.assertRaises(ValueError): wrapper_class(foobar='foobar') + + +class ImmutableListTests(TestCase): + + def test_immutable_list_initial(self): + # Check initial + ilist = ImmutableList() + self.assertEqual(str(ilist), '') + self.assertEqual(repr(ilist), 'ImmutableList()') + self.assertEqual(len(ilist), 0) + + def test_immutable_list_creation(self): + # Check value + ilist = ImmutableList([1]) + self.assertEqual(str(ilist), "1") + self.assertEqual(repr(ilist), "ImmutableList([1])") + self.assertEqual(len(ilist), 1) + + # Check values + ilist = ImmutableList(['1', '2']) + self.assertEqual(str(ilist), "1, 2") + self.assertEqual(repr(ilist), "ImmutableList(['1', '2'])") + self.assertEqual(len(ilist), 2) + + def test_immutable_list_get_item(self): + # Check get item + ilist = ImmutableList(['1', '2']) + self.assertEqual(ilist[0], '1') + self.assertEqual(ilist[-1], '2') + + def test_immutable_list_set_item(self): + # Check immutable + ilist = ImmutableList() + with self.assertRaises(TypeError): + ilist[0] = 'initial' + + def test_immutable_list_equality(self): + # Check equality + ilist1 = ImmutableList(['1', 2]) + ilist2 = ImmutableList(['1', 2]) + ilist3 = ImmutableList([2, '1']) + self.assertEqual(ilist1, ilist2) + self.assertNotEqual(ilist1, ilist3) + + def test_immutable_list_hash(self): + # Check hash + ilist1 = ImmutableList(['1', 2]) + ilist2 = ImmutableList(['1', 2]) + + self.assertEqual(hash(ilist1), hash(ilist2)) + + def test_immutable_list_id(self): + # Check id + ilist1 = ImmutableList(['1', 2]) + ilist2 = ImmutableList(['1', 2]) + self.assertNotEqual(id(ilist1), id(ilist2)) + self.assertNotEqual(id(ilist1), id(ilist1.copy())) + self.assertNotEqual(id(ilist2), id(ilist1.copy())) + + def test_immutable_list_copy(self): + # Check copy + ilist1 = ImmutableList(['1', 2]) + ilist2 = ImmutableList(['1', 2]) + + self.assertEqual(hash(ilist2), hash(ilist1.copy())) + self.assertEqual(ilist1, ilist1.copy()) + + +class CursorTests(TestCase): + + def test_cursor_initial(self): + # Check initial + ilist = Cursor() + self.assertEqual(str(ilist), '') + self.assertEqual(repr(ilist), 'Cursor()') + self.assertEqual(len(ilist), 0) + + def test_cursor_creation(self): + # Check value + ilist = Cursor([1]) + self.assertEqual(str(ilist), "1") + self.assertEqual(repr(ilist), "Cursor([1])") + self.assertEqual(len(ilist), 1) + + # Check values + ilist = Cursor(['1', '2']) + self.assertEqual(str(ilist), "1, 2") + self.assertEqual(repr(ilist), "Cursor(['1', '2'])") + self.assertEqual(len(ilist), 2)