From 9c6cf0aa29ea46a7506cd9fce292393427ddfd9a Mon Sep 17 00:00:00 2001 From: Gonzalo Pena-Castellanos Date: Wed, 8 Jan 2020 09:51:44 -0500 Subject: [PATCH 01/10] Add list property for cursor --- colosseum/constants.py | 44 +++++++++++++++++++++++++++++++- colosseum/declaration.py | 55 ++++++++++++++++++++++++++++++++-------- colosseum/validators.py | 21 ++++++++++++++- 3 files changed, 108 insertions(+), 12 deletions(-) diff --git a/colosseum/constants.py b/colosseum/constants.py index 431c9b6ae..1718be472 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_quote, is_rect, is_uri) class Choices: @@ -509,6 +509,48 @@ 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] + +CURSOR_CHOICES = Choices( + 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, + TRANSPARENT, + validators=[is_uri], + explicit_defaulting_constants=[INHERIT]) + ###################################################################### # 18.4 Dynamic outlines ###################################################################### diff --git a/colosseum/declaration.py b/colosseum/declaration.py index 4b2515373..a9ef449dd 100644 --- a/colosseum/declaration.py +++ b/colosseum/declaration.py @@ -1,22 +1,21 @@ -from . import engine as css_engine -from . import parser +from . import engine as css_engine, parser from .constants import ( # noqa ALIGN_CONTENT_CHOICES, ALIGN_ITEMS_CHOICES, ALIGN_SELF_CHOICES, AUTO, BACKGROUND_COLOR_CHOICES, BORDER_COLLAPSE_CHOICES, BORDER_COLOR_CHOICES, BORDER_SPACING_CHOICES, BORDER_STYLE_CHOICES, BORDER_WIDTH_CHOICES, BOX_OFFSET_CHOICES, CAPTION_SIDE_CHOICES, CLEAR_CHOICES, CLIP_CHOICES, - COLOR_CHOICES, DIRECTION_CHOICES, DISPLAY_CHOICES, EMPTY_CELLS_CHOICES, - FLEX_BASIS_CHOICES, FLEX_DIRECTION_CHOICES, FLEX_GROW_CHOICES, - FLEX_SHRINK_CHOICES, FLEX_START, FLEX_WRAP_CHOICES, FLOAT_CHOICES, - GRID_AUTO_CHOICES, GRID_AUTO_FLOW_CHOICES, GRID_GAP_CHOICES, + COLOR_CHOICES, CURSOR_CHOICES, DIRECTION_CHOICES, DISPLAY_CHOICES, + EMPTY_CELLS_CHOICES, FLEX_BASIS_CHOICES, FLEX_DIRECTION_CHOICES, + FLEX_GROW_CHOICES, FLEX_SHRINK_CHOICES, FLEX_START, FLEX_WRAP_CHOICES, + FLOAT_CHOICES, GRID_AUTO_CHOICES, GRID_AUTO_FLOW_CHOICES, GRID_GAP_CHOICES, GRID_PLACEMENT_CHOICES, GRID_TEMPLATE_AREA_CHOICES, GRID_TEMPLATE_CHOICES, INITIAL, INLINE, INVERT, JUSTIFY_CONTENT_CHOICES, LETTER_SPACING_CHOICES, LTR, MARGIN_CHOICES, MAX_SIZE_CHOICES, MEDIUM, MIN_SIZE_CHOICES, NORMAL, NOWRAP, ORDER_CHOICES, ORPHANS_CHOICES, OUTLINE_COLOR_CHOICES, OUTLINE_STYLE_CHOICES, OUTLINE_WIDTH_CHOICES, OVERFLOW_CHOICES, PADDING_CHOICES, PAGE_BREAK_AFTER_CHOICES, PAGE_BREAK_BEFORE_CHOICES, - PAGE_BREAK_INSIDE_CHOICES, POSITION_CHOICES, QUOTES_CHOICES, ROW, - SEPARATE, SHOW, SIZE_CHOICES, STATIC, STRETCH, TABLE_LAYOUT_CHOICES, + PAGE_BREAK_INSIDE_CHOICES, POSITION_CHOICES, QUOTES_CHOICES, ROW, SEPARATE, + SHOW, SIZE_CHOICES, STATIC, STRETCH, TABLE_LAYOUT_CHOICES, TEXT_ALIGN_CHOICES, TEXT_DECORATION_CHOICES, TEXT_INDENT_CHOICES, TEXT_TRANSFORM_CHOICES, TOP, TRANSPARENT, UNICODE_BIDI_CHOICES, VISIBILITY_CHOICES, VISIBLE, WHITE_SPACE_CHOICES, WIDOWS_CHOICES, @@ -24,7 +23,9 @@ TextAlignInitialValue, default, ) from .exceptions import ValidationError -from .wrappers import Border, BorderBottom, BorderLeft, BorderRight, BorderTop, Outline +from .wrappers import ( + Border, BorderBottom, BorderLeft, BorderRight, BorderTop, Outline, +) _CSS_PROPERTIES = set() @@ -71,6 +72,40 @@ def deleter(self): # Attribute doesn't exist pass + +def validated_list_property(name, choices, initial): + """Define a property holding a list values.""" + if not isinstance(initial, list): + raise ValueError('Initial value must be a list!') + + def getter(self): + return getattr(self, '_%s' % name, initial).copy() + + def setter(self, values): + if not isinstance(values, list): + raise ValueError('Value must be a list!') + + validated_values = [] + for value in values: + try: + validated_values.append(choices.validate(value)) + except ValueError: + raise ValueError("Invalid value '%s' for CSS property '%s'; Valid values are: %s" % ( + value, name, choices + )) + + if validated_values != getattr(self, '_%s' % name, initial): + setattr(self, '_%s' % name, validated_values) + self.dirty = True + + def deleter(self): + try: + delattr(self, '_%s' % name) + self.dirty = True + except AttributeError: + # Attribute doesn't exist + pass + _CSS_PROPERTIES.add(name) return property(getter, setter, deleter) @@ -410,7 +445,7 @@ def __init__(self, **style): # 18. User interface ################################################# # 18.1 Cursors - # cursor + cursor = validated_list_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/validators.py b/colosseum/validators.py index 4ed0aebc2..32f0e8341 100644 --- a/colosseum/validators.py +++ b/colosseum/validators.py @@ -2,6 +2,8 @@ Validate values of different css properties. """ +import re + from . import parser from . import units from .exceptions import ValidationError @@ -142,8 +144,25 @@ def is_quote(value): value = parser.quotes(value) except ValueError: raise ValidationError('Value {value} is not a valid quote'.format(value=value)) +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) + + +is_quote.description = '[ ]+' + + +def is_uri(value): + try: + value = URI_RE.match(value).groups()[0] + except Exception: + raise ValidationError('Uri "{value}" not valid!'.format(value=value)) return value -is_quote.description = '[ ]+' +is_uri.description = '' From b60bcd694f7b99a06e4efa6261a334e45d3de3f1 Mon Sep 17 00:00:00 2001 From: Gonzalo Pena-Castellanos Date: Wed, 8 Jan 2020 09:52:02 -0500 Subject: [PATCH 02/10] Add basic tests for list property and uri validator --- tests/test_declaration.py | 63 +++++++++++++++++++++++++++++++++++++-- tests/test_validators.py | 34 ++++++++++++++++++++- 2 files changed, 94 insertions(+), 3 deletions(-) diff --git a/tests/test_declaration.py b/tests/test_declaration.py index 127447428..10d977587 100644 --- a/tests/test_declaration.py +++ b/tests/test_declaration.py @@ -5,10 +5,11 @@ from colosseum.constants import (AUTO, BLOCK, INHERIT, INITIAL, INLINE, LEFT, REVERT, RIGHT, RTL, TABLE, UNSET, Choices, OtherProperty) -from colosseum.declaration import CSS, validated_property +from colosseum.declaration import (CSS, validated_list_property, + 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 +187,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(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 +775,32 @@ def test_directional_property(self): self.assertEqual(node.style.margin_left, 0) self.assertTrue(node.style.dirty) + def test_list_property(self): + node = TestNode(style=CSS()) + node.layout.dirty = None + + # Default value is [AUTO] + self.assertEqual(node.style.cursor, [AUTO]) + + # Set values + node.style.cursor = ["url('test')", AUTO] + self.assertEqual(node.style.cursor, ["url('test')", AUTO]) + + node.style.cursor = ["url('test')", "url('test2')", AUTO] + self.assertEqual(node.style.cursor, ["url('test')", "url('test2')", AUTO]) + + # Set invalid values + with self.assertRaises(ValueError): + node.style.cursor = ['boom'] + + # Set invalid values + with self.assertRaises(ValueError): + node.style.cursor = ['url( "bla)'] + + # Set invalid value not a list + with self.assertRaises(ValueError): + node.style.cursor = AUTO + def test_set_multiple_properties(self): node = TestNode(style=CSS()) node.layout.dirty = None diff --git a/tests/test_validators.py b/tests/test_validators.py index 19f5bceb9..46f97f933 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -3,7 +3,8 @@ 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_integer, is_number, is_quote, is_rect, + is_uri) from colosseum.wrappers import Quotes @@ -164,3 +165,34 @@ def test_quote_valid(self): def test_quote_invalid(self): with self.assertRaises(ValidationError): is_quote("'<' '>' '{'") + + +class UrlTests(TestCase): + + def test_uri(self): + self.assertEqual(is_uri("url('com')"), "url('com')") + self.assertEqual(is_uri("url('com')"), "url('com')") + self.assertEqual(is_uri("url('com')"), "url('com')") + self.assertEqual(is_uri("url('com')"), "url('com')") + + self.assertEqual(is_uri("url(\"com\")"), "url(\"com\")") + self.assertEqual(is_uri("url( \"com\")"), "url( \"com\")") + self.assertEqual(is_uri("url(\"com\" )"), "url(\"com\" )") + self.assertEqual(is_uri("url( \"com\" )"), "url( \"com\" )") + + self.assertEqual(is_uri("url(com)"), "url(com)") + self.assertEqual(is_uri("url( com)"), "url( com)") + self.assertEqual(is_uri("url(com )"), "url(com )") + self.assertEqual(is_uri("url( com )"), "url( com )") + + with self.assertRaises(ValidationError): + is_uri("url( ' com ' )") + + with self.assertRaises(ValidationError): + is_uri("url( \" com \" )") + + with self.assertRaises(ValidationError): + is_uri("url( com )") + + with self.assertRaises(ValidationError): + is_uri("com") From facb19c2df21e253de6f8abb1cfbdb4f9b2f2a6b Mon Sep 17 00:00:00 2001 From: Gonzalo Pena-Castellanos Date: Wed, 8 Jan 2020 09:55:16 -0500 Subject: [PATCH 03/10] Fix code style --- colosseum/constants.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/colosseum/constants.py b/colosseum/constants.py index 1718be472..f480da98f 100644 --- a/colosseum/constants.py +++ b/colosseum/constants.py @@ -527,7 +527,8 @@ def value(self, context): 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] +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] CURSOR_CHOICES = Choices( AUTO, From 686f7846f716f9c08a507aa2941f454c4459d295 Mon Sep 17 00:00:00 2001 From: Gonzalo Pena-Castellanos Date: Sun, 26 Jan 2020 21:47:41 -0500 Subject: [PATCH 04/10] Add uri and cursor handling --- colosseum/constants.py | 27 +-- colosseum/declaration.py | 12 +- colosseum/parser.py | 130 ++++++++++++++- colosseum/validators.py | 29 +++- colosseum/wrappers.py | 65 ++++++++ tests/test_declaration.py | 49 ++++-- tests/test_parser.py | 334 ++++++++++++++++++++++++++++++++++++++ tests/test_validators.py | 119 +++++++++++--- tests/test_wrappers.py | 93 ++++++++++- 9 files changed, 788 insertions(+), 70 deletions(-) diff --git a/colosseum/constants.py b/colosseum/constants.py index f480da98f..9e33b7c26 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_uri) + is_cursor, is_integer, is_length, is_number, + is_percentage, is_quote, is_rect, is_uri) class Choices: @@ -530,27 +530,8 @@ def value(self, context): 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] -CURSOR_CHOICES = Choices( - 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, - TRANSPARENT, - validators=[is_uri], - explicit_defaulting_constants=[INHERIT]) +# 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 a9ef449dd..f25e801ad 100644 --- a/colosseum/declaration.py +++ b/colosseum/declaration.py @@ -445,7 +445,7 @@ def __init__(self, **style): # 18. User interface ################################################# # 18.1 Cursors - cursor = validated_list_property('cursor', CURSOR_CHOICES, initial=[AUTO]) + cursor = validated_property('cursor', CURSOR_CHOICES, initial=AUTO) # 18.4 Dynamic outlines outline_width = validated_property('outline_width', choices=OUTLINE_WIDTH_CHOICES, initial=MEDIUM) @@ -621,14 +621,10 @@ def keys(self): ###################################################################### def __str__(self): non_default = [] + empty = '' for name in _CSS_PROPERTIES: - try: - non_default.append(( - name.replace('_', '-'), - getattr(self, '_%s' % name) - )) - except AttributeError: - pass + if getattr(self, '_%s' % name, empty) != empty: + non_default.append((name.replace('_', '-'), getattr(self, name))) return "; ".join( "%s: %s" % (name, value) diff --git a/colosseum/parser.py b/colosseum/parser.py index a2b734a6c..ffa47c213 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,131 @@ 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 word + value = value[3:] + + # Single quotes and optional spaces + if value.startswith("('") and value.endswith("')"): + # "('some.url')" + return Uri(value[2:-2]) + elif value.startswith("( '") and value.endswith("')"): + # "( 'some.url')" + return Uri(value[3:-2]) + elif value.startswith("('") and value.endswith("' )"): + # "('some.url' )" + return Uri(value[2:-3]) + elif value.startswith("( '") and value.endswith("' )"): + # "( 'some.url' )" + return Uri(value[3:-3]) + # Double quotes and optional spaces + elif value.startswith('("') and value.endswith('")'): + # '("some.url")' + return Uri(value[2:-2]) + elif value.startswith('( "') and value.endswith('")'): + # '( "some.url")' + return Uri(value[3:-2]) + elif value.startswith('("') and value.endswith('" )'): + # '("some.url" )' + return Uri(value[2:-3]) + elif value.startswith('( "') and value.endswith('" )'): + # '( "some.url" )' + return Uri(value[3:-3].strip()) + # No quotes and optional spaces + elif value.startswith('( ') and value.endswith(' )'): + # '( some.url )' + value = value[2:-2] + if value != value.strip(): + raise ValueError('Invalid url %s' % value) + + return Uri(value) + elif value.startswith('( ') and value.endswith(')'): + # '( some.url)' + value = value[2:-1] + if value != value.strip() or value[-1] in ['"', "'"] or value[0] in ['"', "'"]: + raise ValueError('Invalid url %s' % value) + + return Uri(value) + elif value.startswith('(') and value.endswith(' )'): + # '(some.url )' + value = value[1:-2] + if value != value.strip() or value[-1] in ['"', "'"] or value[0] in ['"', "'"]: + raise ValueError('Invalid url %s' % value) + + return Uri(value) + elif value[1:-1] == value[1:-1].strip(): + # '(some.url)' + value = value[1:-1] + + # Some characters appearing in an unquoted URI, such as parentheses, white space characters, + # single quotes (') and double quotes ("), must be escaped with a backslash so that the + # resulting URI value is a URI token: '\(', '\)' + escape_chars = ['(', ')', ' ', "'", '"'] + for char in escape_chars: + if char in value and '\{char}'.format(char=char) not in value: + raise ValueError('Invalid url %s' % value) + + 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 32f0e8341..d345bf6fe 100644 --- a/colosseum/validators.py +++ b/colosseum/validators.py @@ -4,8 +4,7 @@ import re -from . import parser -from . import units +from . import parser, units from .exceptions import ValidationError @@ -157,12 +156,32 @@ def is_quote(value): def is_uri(value): + """Validate value is .""" try: - value = URI_RE.match(value).groups()[0] - except Exception: - raise ValidationError('Uri "{value}" not valid!'.format(value=value)) + 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..7bbe2815a 100644 --- a/colosseum/wrappers.py +++ b/colosseum/wrappers.py @@ -1,4 +1,5 @@ from collections import OrderedDict +from collections.abc import Sequence class BorderSpacing: @@ -151,3 +152,67 @@ 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(Sequence): + """Immutable list to store list properties.""" + + def __init__(self, iterable=()): + self._data = tuple(iterable) + + def _get_error_message(self, err): + return str(err).replace('tuple', self.__class__.__name__, 1) + + def __eq__(self, other): + return other.__class__ == self.__class__ and self._data == other._data + + def __getitem__(self, index): + try: + return self._data[index] + except Exception as err: + error_msg = self._get_error_message(err) + raise err.__class__(error_msg) + + def __len__(self): + return len(self._data) + + def __hash__(self): + return hash((self.__class__.__name__, self._data)) + + def __repr__(self): + class_name = self.__class__.__name__ + if len(self._data) > 1: + text = '{class_name}([{data}])'.format(data=str(self._data)[1:-1], class_name=class_name) + elif len(self._data) == 1: + text = '{class_name}([{data}])'.format(data=str(self._data)[1:-2], 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._data) + + def copy(self): + return self.__class__(self._data) + + +class Cursor(ImmutableList): + """Immutable list to store cursor property.""" diff --git a/tests/test_declaration.py b/tests/test_declaration.py index 10d977587..4c90be1d7 100644 --- a/tests/test_declaration.py +++ b/tests/test_declaration.py @@ -192,7 +192,7 @@ class MyObject: prop = validated_property('prop', choices=Choices(validators=[is_uri]), initial='url(google.com)') obj = MyObject() - self.assertEqual(obj.prop, 'url(google.com)') + self.assertEqual(str(obj.prop), 'url("google.com")') with self.assertRaises(ValueError): obj.prop = 10 @@ -775,31 +775,56 @@ def test_directional_property(self): self.assertEqual(node.style.margin_left, 0) self.assertTrue(node.style.dirty) - def test_list_property(self): + def test_validated_property_cursor_default_value(self): node = TestNode(style=CSS()) node.layout.dirty = None - # Default value is [AUTO] - self.assertEqual(node.style.cursor, [AUTO]) + 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') - # Set values node.style.cursor = ["url('test')", AUTO] - self.assertEqual(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(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 = 'url( "bla)' + + 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 - # Set invalid values with self.assertRaises(ValueError): node.style.cursor = ['boom'] - # Set invalid values with self.assertRaises(ValueError): node.style.cursor = ['url( "bla)'] - # Set invalid value not a list with self.assertRaises(ValueError): - node.style.cursor = AUTO + node.style.cursor = [AUTO, 'url(google.com)'] def test_set_multiple_properties(self): node = TestNode(style=CSS()) @@ -1208,10 +1233,12 @@ def test_str(self): height=20, margin=(30, 40, 50, 60), display=BLOCK, + cursor=['url(some.cursor.uri)', AUTO] ) 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..056fbe030 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,335 @@ 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")') + + def test_cursor_invalid_order_string_1_item_incomplete_quotes(self): + with self.assertRaises(ValueError): + parser.cursor("url('some.url)") + + with self.assertRaises(ValueError): + parser.cursor("url(some.url')") + + with self.assertRaises(ValueError): + parser.cursor('url("some.url)') + + with self.assertRaises(ValueError): + parser.cursor('url(some.url")') + + def test_cursor_invalid_order_string_1_item_mixed_quotes(self): + with self.assertRaises(ValueError): + parser.cursor("url(\"some.url')") + + with self.assertRaises(ValueError): + parser.cursor("url('some.url\")") + + def test_cursor_invalid_order_string_1_item_incomplete_quotes_with_spaces(self): + with self.assertRaises(ValueError): + parser.cursor("url('some.url )") + + with self.assertRaises(ValueError): + parser.cursor("url(\"some.url )") + + with self.assertRaises(ValueError): + parser.cursor("url( some.url')") + + with self.assertRaises(ValueError): + parser.cursor("url( some.url\")") + + with self.assertRaises(ValueError): + parser.cursor('url( "bla)') + + with self.assertRaises(ValueError): + parser.cursor('url( \'bla)') + + with self.assertRaises(ValueError): + parser.cursor('url(bla\' )') + + def test_url_invalid_single_quotes_too_many_spaces_url_left(self): + with self.assertRaises(ValueError): + parser.uri("url( 'some.url')") + + def test_url_invalid_single_quotes_too_many_spaces_url_right(self): + with self.assertRaises(ValueError): + parser.uri("url('some.url' )") + + def test_url_invalid_single_quotes_too_many_spaces_url_left_right(self): + with self.assertRaises(ValueError): + parser.uri("url( 'some.url' )") + + def test_url_invalid_double_quotes_too_many_spaces_url_left(self): + with self.assertRaises(ValueError): + parser.uri('url( "some.url")') + + def test_url_invalid_double_quotes_too_many_spaces_url_right(self): + with self.assertRaises(ValueError): + parser.uri('url("some.url" )') + + def test_url_invalid_double_quotes_too_many_spaces_url_left_right(self): + with self.assertRaises(ValueError): + parser.uri('url( "some.url" )') + + def test_url_invalid_no_quotes_too_many_spaces_url_left(self): + with self.assertRaises(ValueError): + parser.uri('url( some.url)') + + def test_url_invalid_no_quotes_too_many_spaces_url_right(self): + with self.assertRaises(ValueError): + parser.uri('url(some.url )') + + def test_url_invalid_no_quotes_too_many_spaces_url_left_right(self): + with self.assertRaises(ValueError): + parser.uri('url( some.url )') + + def test_url_invalid_no_quotes_not_escaped_chars(self): + with self.assertRaises(ValueError): + parser.uri('url(some.(url)') + + with self.assertRaises(ValueError): + parser.uri('url(some.)url)') + + with self.assertRaises(ValueError): + parser.uri('url(some. url)') + + with self.assertRaises(ValueError): + parser.uri('url(some."url)') + + with self.assertRaises(ValueError): + parser.uri("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_1_item_invalid_uri(self): + with self.assertRaises(ValueError): + parser.cursor("url( some.uri )") + + 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_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(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_1_item_invalid_uri(self): + with self.assertRaises(ValueError): + parser.cursor(["url( some.uri )"]) + + 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 46f97f933..2f7697af7 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -3,8 +3,8 @@ 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_uri) + is_cursor, is_integer, is_number, is_quote, + is_rect, is_uri) from colosseum.wrappers import Quotes @@ -167,32 +167,111 @@ def test_quote_invalid(self): is_quote("'<' '>' '{'") -class UrlTests(TestCase): +class UriTests(TestCase): + """Comprehensive tests are found on test_parser.py.""" - def test_uri(self): - self.assertEqual(is_uri("url('com')"), "url('com')") - self.assertEqual(is_uri("url('com')"), "url('com')") - self.assertEqual(is_uri("url('com')"), "url('com')") - self.assertEqual(is_uri("url('com')"), "url('com')") + def test_url_valid(self): + url = is_uri("url(some.url)") + self.assertEqual(str(url), 'url("some.url")') - self.assertEqual(is_uri("url(\"com\")"), "url(\"com\")") - self.assertEqual(is_uri("url( \"com\")"), "url( \"com\")") - self.assertEqual(is_uri("url(\"com\" )"), "url(\"com\" )") - self.assertEqual(is_uri("url( \"com\" )"), "url( \"com\" )") + url = is_uri(" url(some.url) ") + self.assertEqual(str(url), 'url("some.url")') - self.assertEqual(is_uri("url(com)"), "url(com)") - self.assertEqual(is_uri("url( com)"), "url( com)") - self.assertEqual(is_uri("url(com )"), "url(com )") - self.assertEqual(is_uri("url( com )"), "url( com )") + 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")') + + def test_url_invalid(self): + with self.assertRaises(ValidationError): + is_uri("url(some. url)") + + with self.assertRaises(ValidationError): + is_uri("url( some.url )") + + with self.assertRaises(ValidationError): + is_uri("url( 'some.url' )") + + with self.assertRaises(ValidationError): + is_uri('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("url( something )") + + with self.assertRaises(ValidationError): + is_cursor(["foobar"]) + + with self.assertRaises(ValidationError): + is_cursor(["url( something )"]) + + def test_cursor_invalid_2_items(self): + with self.assertRaises(ValidationError): + is_cursor("foobar, blah") + + with self.assertRaises(ValidationError): + is_cursor("url(something), url( something )") + + with self.assertRaises(ValidationError): + is_cursor("auto, url( something )") with self.assertRaises(ValidationError): - is_uri("url( ' com ' )") + is_cursor(["foobar", 'blah']) with self.assertRaises(ValidationError): - is_uri("url( \" com \" )") + is_cursor(["url(something)", "url( something )"]) with self.assertRaises(ValidationError): - is_uri("url( com )") + is_cursor(["auto", "url(something)"]) with self.assertRaises(ValidationError): - is_uri("com") + 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) From 96269818c8af22889ec20ad0fa9c0c09692c80e1 Mon Sep 17 00:00:00 2001 From: Gonzalo Pena-Castellanos Date: Sun, 26 Jan 2020 22:53:28 -0500 Subject: [PATCH 05/10] Fix code style --- colosseum/declaration.py | 2 +- colosseum/parser.py | 2 +- tests/test_declaration.py | 2 +- tests/test_parser.py | 6 +----- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/colosseum/declaration.py b/colosseum/declaration.py index f25e801ad..2b0b883cf 100644 --- a/colosseum/declaration.py +++ b/colosseum/declaration.py @@ -623,7 +623,7 @@ def __str__(self): non_default = [] empty = '' for name in _CSS_PROPERTIES: - if getattr(self, '_%s' % name, empty) != empty: + if getattr(self, '_%s' % name, empty) != empty: non_default.append((name.replace('_', '-'), getattr(self, name))) return "; ".join( diff --git a/colosseum/parser.py b/colosseum/parser.py index ffa47c213..e3c24b825 100644 --- a/colosseum/parser.py +++ b/colosseum/parser.py @@ -451,7 +451,7 @@ def uri(value): # resulting URI value is a URI token: '\(', '\)' escape_chars = ['(', ')', ' ', "'", '"'] for char in escape_chars: - if char in value and '\{char}'.format(char=char) not in value: + if char in value and r'\{char}'.format(char=char) not in value: raise ValueError('Invalid url %s' % value) return Uri(value) diff --git a/tests/test_declaration.py b/tests/test_declaration.py index 4c90be1d7..0f7a3de5d 100644 --- a/tests/test_declaration.py +++ b/tests/test_declaration.py @@ -1233,7 +1233,7 @@ def test_str(self): height=20, margin=(30, 40, 50, 60), display=BLOCK, - cursor=['url(some.cursor.uri)', AUTO] + cursor=['url(some.cursor.uri)', AUTO] ) self.assertEqual( diff --git a/tests/test_parser.py b/tests/test_parser.py index 056fbe030..8facfb425 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -925,10 +925,6 @@ 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_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(self): for option in CURSOR_OPTIONS: with self.assertRaises(ValueError): @@ -953,7 +949,7 @@ def test_cursor_invalid_order_string_3_items_option_3(self): for (option1, option2, option3) in perms: with self.assertRaises(ValueError): parser.cursor("{option1}, {option2}, {option3}".format(option1=option1, option2=option2, - option3=option3)) + option3=option3)) def test_cursor_invalid_order_string_3_items_option_2_uri_1(self): perms = permutations(CURSOR_OPTIONS, 2) From 2f416ac090dbc09afbde46bb8f268a2daad38451 Mon Sep 17 00:00:00 2001 From: Gonzalo Pena-Castellanos Date: Mon, 27 Jan 2020 00:11:21 -0500 Subject: [PATCH 06/10] Fix code style --- colosseum/declaration.py | 2 +- tests/test_validators.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/colosseum/declaration.py b/colosseum/declaration.py index 2b0b883cf..ee2f01a6a 100644 --- a/colosseum/declaration.py +++ b/colosseum/declaration.py @@ -146,7 +146,7 @@ def validated_property(name, choices, initial): # Check the value attribute is a callable if not callable(value_attr): - raise ValueError('Initial value "%s" `value` attribute is not callable!' % initial) + raise ValueError('Initial value "%s" attribute is not callable!' % initial) except AttributeError: raise ValueError('Initial value "%s" does not have a value attribute!' % initial) diff --git a/tests/test_validators.py b/tests/test_validators.py index 2f7697af7..048c69496 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -7,7 +7,6 @@ is_rect, is_uri) from colosseum.wrappers import Quotes - class NumericTests(TestCase): def test_integer(self): From 8b8d8f26cf36ef327e83b001bb752898471b8a13 Mon Sep 17 00:00:00 2001 From: Gonzalo Pena-Castellanos Date: Mon, 27 Jan 2020 04:10:35 -0500 Subject: [PATCH 07/10] Reverse CSS str, simplify parser, update tests --- colosseum/declaration.py | 6 ++-- colosseum/parser.py | 63 ++++++------------------------------- tests/test_parser.py | 68 +++++++--------------------------------- tests/test_validators.py | 16 +++++----- 4 files changed, 34 insertions(+), 119 deletions(-) diff --git a/colosseum/declaration.py b/colosseum/declaration.py index ee2f01a6a..4971e4900 100644 --- a/colosseum/declaration.py +++ b/colosseum/declaration.py @@ -621,10 +621,12 @@ def keys(self): ###################################################################### def __str__(self): non_default = [] - empty = '' for name in _CSS_PROPERTIES: - if getattr(self, '_%s' % name, empty) != empty: + try: + getattr(self, '_%s' % name) non_default.append((name.replace('_', '-'), getattr(self, name))) + except AttributeError: + pass return "; ".join( "%s: %s" % (name, value) diff --git a/colosseum/parser.py b/colosseum/parser.py index e3c24b825..6958ca80a 100644 --- a/colosseum/parser.py +++ b/colosseum/parser.py @@ -379,11 +379,11 @@ def uri(value): Accepts: * url("") - * url( "" ) + * url( "" ) * url('') - * url( '' ) + * url( '' ) * url() - * url( ) + * url( ) """ if isinstance(value, str): value = value.strip() @@ -391,61 +391,18 @@ def uri(value): raise ValueError('Value {value} must be a string') if value.startswith('url(') and value.endswith(')'): - # Remove the url word - value = value[3:] + # Remove the 'url(' and ')' + value = value[4:-1].strip() # Single quotes and optional spaces - if value.startswith("('") and value.endswith("')"): - # "('some.url')" - return Uri(value[2:-2]) - elif value.startswith("( '") and value.endswith("')"): - # "( 'some.url')" - return Uri(value[3:-2]) - elif value.startswith("('") and value.endswith("' )"): - # "('some.url' )" - return Uri(value[2:-3]) - elif value.startswith("( '") and value.endswith("' )"): - # "( 'some.url' )" - return Uri(value[3:-3]) + if value.startswith("'") and value.endswith("'"): + return Uri(value[1:-1]) # Double quotes and optional spaces - elif value.startswith('("') and value.endswith('")'): + elif value.startswith('"') and value.endswith('"'): # '("some.url")' - return Uri(value[2:-2]) - elif value.startswith('( "') and value.endswith('")'): - # '( "some.url")' - return Uri(value[3:-2]) - elif value.startswith('("') and value.endswith('" )'): - # '("some.url" )' - return Uri(value[2:-3]) - elif value.startswith('( "') and value.endswith('" )'): - # '( "some.url" )' - return Uri(value[3:-3].strip()) + return Uri(value[1:-1]) # No quotes and optional spaces - elif value.startswith('( ') and value.endswith(' )'): - # '( some.url )' - value = value[2:-2] - if value != value.strip(): - raise ValueError('Invalid url %s' % value) - - return Uri(value) - elif value.startswith('( ') and value.endswith(')'): - # '( some.url)' - value = value[2:-1] - if value != value.strip() or value[-1] in ['"', "'"] or value[0] in ['"', "'"]: - raise ValueError('Invalid url %s' % value) - - return Uri(value) - elif value.startswith('(') and value.endswith(' )'): - # '(some.url )' - value = value[1:-2] - if value != value.strip() or value[-1] in ['"', "'"] or value[0] in ['"', "'"]: - raise ValueError('Invalid url %s' % value) - - return Uri(value) - elif value[1:-1] == value[1:-1].strip(): - # '(some.url)' - value = value[1:-1] - + else: # Some characters appearing in an unquoted URI, such as parentheses, white space characters, # single quotes (') and double quotes ("), must be escaped with a backslash so that the # resulting URI value is a URI token: '\(', '\)' diff --git a/tests/test_parser.py b/tests/test_parser.py index 8facfb425..5488d45c6 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -688,15 +688,15 @@ def test_url_valid_single_quotes_url(self): self.assertEqual(str(url), 'url("some.url")') def test_url_valid_single_quotes_spaces_url_left(self): - url = parser.uri("url( 'some.url')") + 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' )") + 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' )") + url = parser.uri("url( 'some.url' )") self.assertEqual(str(url), 'url("some.url")') def test_url_valid_double_quotes_url(self): @@ -704,27 +704,27 @@ def test_url_valid_double_quotes_url(self): self.assertEqual(str(url), 'url("some.url")') def test_url_valid_double_quotes_spaces_url_left(self): - url = parser.uri('url( "some.url")') + 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" )') + 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" )') + 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 )') + 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)') + 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 )') + url = parser.uri('url(some.url )') self.assertEqual(str(url), 'url("some.url")') def test_url_valid_no_quotes_url(self): @@ -781,49 +781,13 @@ def test_cursor_invalid_order_string_1_item_incomplete_quotes_with_spaces(self): parser.cursor("url( some.url\")") with self.assertRaises(ValueError): - parser.cursor('url( "bla)') + parser.cursor('url( "bla)') with self.assertRaises(ValueError): - parser.cursor('url( \'bla)') + parser.cursor('url( \'bla)') with self.assertRaises(ValueError): - parser.cursor('url(bla\' )') - - def test_url_invalid_single_quotes_too_many_spaces_url_left(self): - with self.assertRaises(ValueError): - parser.uri("url( 'some.url')") - - def test_url_invalid_single_quotes_too_many_spaces_url_right(self): - with self.assertRaises(ValueError): - parser.uri("url('some.url' )") - - def test_url_invalid_single_quotes_too_many_spaces_url_left_right(self): - with self.assertRaises(ValueError): - parser.uri("url( 'some.url' )") - - def test_url_invalid_double_quotes_too_many_spaces_url_left(self): - with self.assertRaises(ValueError): - parser.uri('url( "some.url")') - - def test_url_invalid_double_quotes_too_many_spaces_url_right(self): - with self.assertRaises(ValueError): - parser.uri('url("some.url" )') - - def test_url_invalid_double_quotes_too_many_spaces_url_left_right(self): - with self.assertRaises(ValueError): - parser.uri('url( "some.url" )') - - def test_url_invalid_no_quotes_too_many_spaces_url_left(self): - with self.assertRaises(ValueError): - parser.uri('url( some.url)') - - def test_url_invalid_no_quotes_too_many_spaces_url_right(self): - with self.assertRaises(ValueError): - parser.uri('url(some.url )') - - def test_url_invalid_no_quotes_too_many_spaces_url_left_right(self): - with self.assertRaises(ValueError): - parser.uri('url( some.url )') + parser.cursor('url(bla\' )') def test_url_invalid_no_quotes_not_escaped_chars(self): with self.assertRaises(ValueError): @@ -913,10 +877,6 @@ def test_cursor_invalid_order_string_1_item_invalid_value(self): with self.assertRaises(ValueError): parser.cursor("foobar") - def test_cursor_invalid_order_string_1_item_invalid_uri(self): - with self.assertRaises(ValueError): - parser.cursor("url( some.uri )") - 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 )") @@ -964,10 +924,6 @@ def test_cursor_invalid_order_list_1_item_invalid_value(self): with self.assertRaises(ValueError): parser.cursor(["foobar"]) - def test_cursor_invalid_order_list_1_item_invalid_uri(self): - with self.assertRaises(ValueError): - parser.cursor(["url( some.uri )"]) - 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 )"]) diff --git a/tests/test_validators.py b/tests/test_validators.py index 048c69496..f184718cf 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -196,13 +196,13 @@ def test_url_invalid(self): is_uri("url(some. url)") with self.assertRaises(ValidationError): - is_uri("url( some.url )") + is_uri("url( some.url( )") with self.assertRaises(ValidationError): - is_uri("url( 'some.url' )") + is_uri("url( )'some.url' )") with self.assertRaises(ValidationError): - is_uri('url( "some.url" )') + is_uri('url( "some.url"\' )') class CursorTests(TestCase): @@ -245,29 +245,29 @@ def test_cursor_invalid_1_item(self): is_cursor("foobar") with self.assertRaises(ValidationError): - is_cursor("url( something )") + is_cursor("url( (something )") with self.assertRaises(ValidationError): is_cursor(["foobar"]) with self.assertRaises(ValidationError): - is_cursor(["url( something )"]) + is_cursor(["url( 'something )"]) def test_cursor_invalid_2_items(self): with self.assertRaises(ValidationError): is_cursor("foobar, blah") with self.assertRaises(ValidationError): - is_cursor("url(something), url( something )") + is_cursor("url(something), url( something' )") with self.assertRaises(ValidationError): - is_cursor("auto, url( something )") + is_cursor("auto, url( something' )") with self.assertRaises(ValidationError): is_cursor(["foobar", 'blah']) with self.assertRaises(ValidationError): - is_cursor(["url(something)", "url( something )"]) + is_cursor(["url(something)", "url( 'something )"]) with self.assertRaises(ValidationError): is_cursor(["auto", "url(something)"]) From d294c3502e8cd4a31c6345c109b845bd241aaa0d Mon Sep 17 00:00:00 2001 From: goanpeca Date: Sun, 16 Feb 2020 23:04:22 -0500 Subject: [PATCH 08/10] Fix code style --- colosseum/declaration.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/colosseum/declaration.py b/colosseum/declaration.py index 4971e4900..6aed676c2 100644 --- a/colosseum/declaration.py +++ b/colosseum/declaration.py @@ -623,8 +623,10 @@ def __str__(self): non_default = [] for name in _CSS_PROPERTIES: try: - getattr(self, '_%s' % name) - non_default.append((name.replace('_', '-'), getattr(self, name))) + non_default.append(( + name.replace('_', '-'), + getattr(self, '_%s' % name) + )) except AttributeError: pass From f5cf9a2e1caa6fd5c68464ba269da900e8064224 Mon Sep 17 00:00:00 2001 From: goanpeca Date: Sun, 16 Feb 2020 23:24:52 -0500 Subject: [PATCH 09/10] Fix tests --- colosseum/parser.py | 8 ------ tests/test_declaration.py | 8 ++---- tests/test_parser.py | 58 --------------------------------------- tests/test_validators.py | 27 +----------------- 4 files changed, 3 insertions(+), 98 deletions(-) diff --git a/colosseum/parser.py b/colosseum/parser.py index 6958ca80a..310450fd8 100644 --- a/colosseum/parser.py +++ b/colosseum/parser.py @@ -403,14 +403,6 @@ def uri(value): return Uri(value[1:-1]) # No quotes and optional spaces else: - # Some characters appearing in an unquoted URI, such as parentheses, white space characters, - # single quotes (') and double quotes ("), must be escaped with a backslash so that the - # resulting URI value is a URI token: '\(', '\)' - escape_chars = ['(', ')', ' ', "'", '"'] - for char in escape_chars: - if char in value and r'\{char}'.format(char=char) not in value: - raise ValueError('Invalid url %s' % value) - return Uri(value) raise ValueError('Invalid url %s' % value) diff --git a/tests/test_declaration.py b/tests/test_declaration.py index 0f7a3de5d..13f835094 100644 --- a/tests/test_declaration.py +++ b/tests/test_declaration.py @@ -804,9 +804,6 @@ def test_validated_property_cursor_set_invalid_str_values(self): with self.assertRaises(ValueError): node.style.cursor = 'boom' - with self.assertRaises(ValueError): - node.style.cursor = 'url( "bla)' - with self.assertRaises(ValueError): node.style.cursor = 'auto, url(google.com)' @@ -820,9 +817,6 @@ def test_validated_property_cursor_set_invalid_list_values(self): with self.assertRaises(ValueError): node.style.cursor = ['boom'] - with self.assertRaises(ValueError): - node.style.cursor = ['url( "bla)'] - with self.assertRaises(ValueError): node.style.cursor = [AUTO, 'url(google.com)'] @@ -1236,6 +1230,8 @@ def test_str(self): cursor=['url(some.cursor.uri)', AUTO] ) + print(str(node.style)) + self.assertEqual( str(node.style), 'cursor: url("some.cursor.uri"), auto; ' diff --git a/tests/test_parser.py b/tests/test_parser.py index 5488d45c6..d1947d3e4 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -747,64 +747,6 @@ def test_url_valid_no_quotes_escaped_chars(self): url = parser.uri(r"url(some.\'url)") self.assertEqual(str(url), r'url("some.\'url")') - def test_cursor_invalid_order_string_1_item_incomplete_quotes(self): - with self.assertRaises(ValueError): - parser.cursor("url('some.url)") - - with self.assertRaises(ValueError): - parser.cursor("url(some.url')") - - with self.assertRaises(ValueError): - parser.cursor('url("some.url)') - - with self.assertRaises(ValueError): - parser.cursor('url(some.url")') - - def test_cursor_invalid_order_string_1_item_mixed_quotes(self): - with self.assertRaises(ValueError): - parser.cursor("url(\"some.url')") - - with self.assertRaises(ValueError): - parser.cursor("url('some.url\")") - - def test_cursor_invalid_order_string_1_item_incomplete_quotes_with_spaces(self): - with self.assertRaises(ValueError): - parser.cursor("url('some.url )") - - with self.assertRaises(ValueError): - parser.cursor("url(\"some.url )") - - with self.assertRaises(ValueError): - parser.cursor("url( some.url')") - - with self.assertRaises(ValueError): - parser.cursor("url( some.url\")") - - with self.assertRaises(ValueError): - parser.cursor('url( "bla)') - - with self.assertRaises(ValueError): - parser.cursor('url( \'bla)') - - with self.assertRaises(ValueError): - parser.cursor('url(bla\' )') - - def test_url_invalid_no_quotes_not_escaped_chars(self): - with self.assertRaises(ValueError): - parser.uri('url(some.(url)') - - with self.assertRaises(ValueError): - parser.uri('url(some.)url)') - - with self.assertRaises(ValueError): - parser.uri('url(some. url)') - - with self.assertRaises(ValueError): - parser.uri('url(some."url)') - - with self.assertRaises(ValueError): - parser.uri("url(some.'url)") - class ParseCursorTests(TestCase): diff --git a/tests/test_validators.py b/tests/test_validators.py index f184718cf..1d7e5322c 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -191,19 +191,6 @@ def test_url_valid(self): url = is_uri('url( "some.url" )') self.assertEqual(str(url), 'url("some.url")') - def test_url_invalid(self): - with self.assertRaises(ValidationError): - is_uri("url(some. url)") - - with self.assertRaises(ValidationError): - is_uri("url( some.url( )") - - with self.assertRaises(ValidationError): - is_uri("url( )'some.url' )") - - with self.assertRaises(ValidationError): - is_uri('url( "some.url"\' )') - class CursorTests(TestCase): """Comprehensive tests are found on test_parser.py.""" @@ -244,31 +231,19 @@ def test_cursor_invalid_1_item(self): with self.assertRaises(ValidationError): is_cursor("foobar") - with self.assertRaises(ValidationError): - is_cursor("url( (something )") - with self.assertRaises(ValidationError): is_cursor(["foobar"]) - with self.assertRaises(ValidationError): - is_cursor(["url( 'something )"]) - def test_cursor_invalid_2_items(self): with self.assertRaises(ValidationError): is_cursor("foobar, blah") with self.assertRaises(ValidationError): - is_cursor("url(something), url( something' )") - - with self.assertRaises(ValidationError): - is_cursor("auto, url( something' )") + is_cursor("auto, url( something )") with self.assertRaises(ValidationError): is_cursor(["foobar", 'blah']) - with self.assertRaises(ValidationError): - is_cursor(["url(something)", "url( 'something )"]) - with self.assertRaises(ValidationError): is_cursor(["auto", "url(something)"]) From aeebebab912999b45926d3661a6b49dfcbfbce10 Mon Sep 17 00:00:00 2001 From: goanpeca Date: Sun, 26 Apr 2020 20:51:36 -0500 Subject: [PATCH 10/10] Subclass list --- colosseum/constants.py | 2 +- colosseum/declaration.py | 57 ++++++++------------------------------- colosseum/parser.py | 1 - colosseum/validators.py | 10 ++++--- colosseum/wrappers.py | 53 ++++++++++++++++++++++++------------ tests/test_declaration.py | 3 +-- tests/test_parser.py | 2 ++ 7 files changed, 58 insertions(+), 70 deletions(-) diff --git a/colosseum/constants.py b/colosseum/constants.py index 9e33b7c26..e5a45e5fc 100644 --- a/colosseum/constants.py +++ b/colosseum/constants.py @@ -1,6 +1,6 @@ from .validators import (ValidationError, is_border_spacing, is_color, is_cursor, is_integer, is_length, is_number, - is_percentage, is_quote, is_rect, is_uri) + is_percentage, is_quote, is_rect) class Choices: diff --git a/colosseum/declaration.py b/colosseum/declaration.py index 6aed676c2..40320cbb8 100644 --- a/colosseum/declaration.py +++ b/colosseum/declaration.py @@ -1,31 +1,30 @@ -from . import engine as css_engine, parser +from . import engine as css_engine +from . import parser from .constants import ( # noqa ALIGN_CONTENT_CHOICES, ALIGN_ITEMS_CHOICES, ALIGN_SELF_CHOICES, AUTO, BACKGROUND_COLOR_CHOICES, BORDER_COLLAPSE_CHOICES, BORDER_COLOR_CHOICES, BORDER_SPACING_CHOICES, BORDER_STYLE_CHOICES, BORDER_WIDTH_CHOICES, BOX_OFFSET_CHOICES, CAPTION_SIDE_CHOICES, CLEAR_CHOICES, CLIP_CHOICES, - COLOR_CHOICES, CURSOR_CHOICES, DIRECTION_CHOICES, DISPLAY_CHOICES, - EMPTY_CELLS_CHOICES, FLEX_BASIS_CHOICES, FLEX_DIRECTION_CHOICES, - FLEX_GROW_CHOICES, FLEX_SHRINK_CHOICES, FLEX_START, FLEX_WRAP_CHOICES, - FLOAT_CHOICES, GRID_AUTO_CHOICES, GRID_AUTO_FLOW_CHOICES, GRID_GAP_CHOICES, + COLOR_CHOICES, DIRECTION_CHOICES, DISPLAY_CHOICES, EMPTY_CELLS_CHOICES, + FLEX_BASIS_CHOICES, FLEX_DIRECTION_CHOICES, FLEX_GROW_CHOICES, + FLEX_SHRINK_CHOICES, FLEX_START, FLEX_WRAP_CHOICES, FLOAT_CHOICES, + GRID_AUTO_CHOICES, GRID_AUTO_FLOW_CHOICES, GRID_GAP_CHOICES, GRID_PLACEMENT_CHOICES, GRID_TEMPLATE_AREA_CHOICES, GRID_TEMPLATE_CHOICES, INITIAL, INLINE, INVERT, JUSTIFY_CONTENT_CHOICES, LETTER_SPACING_CHOICES, LTR, MARGIN_CHOICES, MAX_SIZE_CHOICES, MEDIUM, MIN_SIZE_CHOICES, NORMAL, NOWRAP, ORDER_CHOICES, ORPHANS_CHOICES, OUTLINE_COLOR_CHOICES, OUTLINE_STYLE_CHOICES, OUTLINE_WIDTH_CHOICES, OVERFLOW_CHOICES, PADDING_CHOICES, PAGE_BREAK_AFTER_CHOICES, PAGE_BREAK_BEFORE_CHOICES, - PAGE_BREAK_INSIDE_CHOICES, POSITION_CHOICES, QUOTES_CHOICES, ROW, SEPARATE, - SHOW, SIZE_CHOICES, STATIC, STRETCH, TABLE_LAYOUT_CHOICES, + PAGE_BREAK_INSIDE_CHOICES, POSITION_CHOICES, QUOTES_CHOICES, ROW, + SEPARATE, SHOW, SIZE_CHOICES, STATIC, STRETCH, TABLE_LAYOUT_CHOICES, TEXT_ALIGN_CHOICES, TEXT_DECORATION_CHOICES, TEXT_INDENT_CHOICES, 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, -) +from .wrappers import Border, BorderBottom, BorderLeft, BorderRight, BorderTop, Outline _CSS_PROPERTIES = set() @@ -72,40 +71,6 @@ def deleter(self): # Attribute doesn't exist pass - -def validated_list_property(name, choices, initial): - """Define a property holding a list values.""" - if not isinstance(initial, list): - raise ValueError('Initial value must be a list!') - - def getter(self): - return getattr(self, '_%s' % name, initial).copy() - - def setter(self, values): - if not isinstance(values, list): - raise ValueError('Value must be a list!') - - validated_values = [] - for value in values: - try: - validated_values.append(choices.validate(value)) - except ValueError: - raise ValueError("Invalid value '%s' for CSS property '%s'; Valid values are: %s" % ( - value, name, choices - )) - - if validated_values != getattr(self, '_%s' % name, initial): - setattr(self, '_%s' % name, validated_values) - self.dirty = True - - def deleter(self): - try: - delattr(self, '_%s' % name) - self.dirty = True - except AttributeError: - # Attribute doesn't exist - pass - _CSS_PROPERTIES.add(name) return property(getter, setter, deleter) @@ -146,7 +111,7 @@ def validated_property(name, choices, initial): # Check the value attribute is a callable if not callable(value_attr): - raise ValueError('Initial value "%s" attribute is not callable!' % initial) + raise ValueError('Initial value "%s" `value` attribute is not callable!' % initial) except AttributeError: raise ValueError('Initial value "%s" does not have a value attribute!' % initial) diff --git a/colosseum/parser.py b/colosseum/parser.py index 310450fd8..85b7acb8a 100644 --- a/colosseum/parser.py +++ b/colosseum/parser.py @@ -408,7 +408,6 @@ def uri(value): raise ValueError('Invalid url %s' % value) - ############################################################################## # Cursor ############################################################################## diff --git a/colosseum/validators.py b/colosseum/validators.py index d345bf6fe..7ba8ec978 100644 --- a/colosseum/validators.py +++ b/colosseum/validators.py @@ -143,6 +143,13 @@ def is_quote(value): value = parser.quotes(value) except ValueError: raise ValidationError('Value {value} is not a valid quote'.format(value=value)) + + return value + + +is_quote.description = '[ ]+' + + URI_RE = re.compile(r"""( (?:url\(\s?'[A-Za-z0-9\./\:\?]*'\s?\)) # Single quotes and optional spaces | @@ -152,9 +159,6 @@ def is_quote(value): )""", re.VERBOSE) -is_quote.description = '[ ]+' - - def is_uri(value): """Validate value is .""" try: diff --git a/colosseum/wrappers.py b/colosseum/wrappers.py index 7bbe2815a..05809ff1c 100644 --- a/colosseum/wrappers.py +++ b/colosseum/wrappers.py @@ -1,5 +1,4 @@ from collections import OrderedDict -from collections.abc import Sequence class BorderSpacing: @@ -154,7 +153,6 @@ class Border(Shorthand): VALID_KEYS = ['border_width', 'border_style', 'border_color'] - class Uri: """Wrapper for a url.""" @@ -172,46 +170,67 @@ def url(self): return self._url -class ImmutableList(Sequence): +class ImmutableList(list): """Immutable list to store list properties.""" def __init__(self, iterable=()): - self._data = tuple(iterable) + super().__init__(iterable) def _get_error_message(self, err): - return str(err).replace('tuple', self.__class__.__name__, 1) + return str(err).replace('list', self.__class__.__name__, 1) - def __eq__(self, other): - return other.__class__ == self.__class__ and self._data == other._data + # def __eq__(self, other): + # return other.__class__ == self.__class__ and self == other def __getitem__(self, index): try: - return self._data[index] + return super().__getitem__(index) except Exception as err: error_msg = self._get_error_message(err) raise err.__class__(error_msg) - def __len__(self): - return len(self._data) + def __setitem__(self, index, value): + raise TypeError("{} values cannot be changed!".format(self.__class__.__name__)) def __hash__(self): - return hash((self.__class__.__name__, self._data)) + return hash((self.__class__.__name__, tuple(self))) def __repr__(self): class_name = self.__class__.__name__ - if len(self._data) > 1: - text = '{class_name}([{data}])'.format(data=str(self._data)[1:-1], class_name=class_name) - elif len(self._data) == 1: - text = '{class_name}([{data}])'.format(data=str(self._data)[1:-2], class_name=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._data) + return ', '.join(str(v) for v in self) def copy(self): - return self.__class__(self._data) + 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): diff --git a/tests/test_declaration.py b/tests/test_declaration.py index 13f835094..d018b3ce3 100644 --- a/tests/test_declaration.py +++ b/tests/test_declaration.py @@ -5,8 +5,7 @@ from colosseum.constants import (AUTO, BLOCK, INHERIT, INITIAL, INLINE, LEFT, REVERT, RIGHT, RTL, TABLE, UNSET, Choices, OtherProperty) -from colosseum.declaration import (CSS, validated_list_property, - validated_property) +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_uri) diff --git a/tests/test_parser.py b/tests/test_parser.py index d1947d3e4..941e4e511 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -681,6 +681,8 @@ 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):