From 21a927046d43b37c883973c10efc54b43d70d646 Mon Sep 17 00:00:00 2001 From: s3lph Date: Fri, 29 Jun 2018 18:29:51 +0200 Subject: [PATCH] Reworked RequestArgument API to somewhat more lax concerning 0-indices, potentially leading to safer code. --- matemat/webserver/requestargs.py | 177 +++++++-------------- matemat/webserver/test/test_requestargs.py | 66 ++++---- 2 files changed, 94 insertions(+), 149 deletions(-) diff --git a/matemat/webserver/requestargs.py b/matemat/webserver/requestargs.py index 1a56aeb..dcad518 100644 --- a/matemat/webserver/requestargs.py +++ b/matemat/webserver/requestargs.py @@ -43,35 +43,31 @@ class RequestArgument(object): # Assign name self.__name: str = name # Initialize value - self.__value: Union[Tuple[str, Union[bytes, str]], List[Tuple[str, Union[bytes, str]]]] = [] + self.__value: List[Tuple[str, Union[bytes, str]]] = [] # Default to empty array if value is None: self.__value = [] else: if isinstance(value, list): - if len(value) == 1: - # An array of length 1 will be reduced to a scalar - self.__value = value[0] - else: - # Store the array - self.__value = value - else: - # Scalar value, simply store + # Store the array self.__value = value + else: + # Turn scalar into an array before storing + self.__value = [value] @property def is_array(self) -> bool: """ :return: True, if the value is a (possibly empty) array, False otherwise. """ - return isinstance(self.__value, list) + return len(self.__value) != 1 @property def is_scalar(self) -> bool: """ :return: True, if the value is a single scalar value, False otherwise. """ - return not isinstance(self.__value, list) + return len(self.__value) == 1 @property def is_view(self) -> bool: @@ -87,112 +83,70 @@ class RequestArgument(object): """ return self.__name - def get_str(self, index: int = None) -> str: + def get_str(self, index: int = 0) -> str: """ - Attempts to return a value as a string. If this instance is an scalar, no index must be provided. If this - instance is an array, an index must be provided. + Attempts to return a value as a string. The index defaults to 0. - :param index: For array values: The index of the value to retrieve. For scalar values: None (default). + :param index: The index of the value to retrieve. Default: 0. :return: An UTF-8 string representation of the requested value. :raises UnicodeDecodeError: If the value cannot be decoded into an UTF-8 string. :raises IndexError: If the index is out of bounds. - :raises ValueError: If this is an array value, and no index is provided, or if this is a scalar value and an - index is provided. + :raises ValueError: If the index is not an int. :raises TypeError: If the requested value is neither a str nor a bytes object. """ - if self.is_array: - # instance is an array value - if index is None: - # Needs an index for array values - raise ValueError('index must not be None') - # Type hint; access array element - v: Tuple[str, Union[bytes, str]] = self.__value[index] - if isinstance(v[1], str): - # The value already is a string, return - return v[1] - elif isinstance(v[1], bytes): - # The value is a bytes object, attempt to decode - return v[1].decode('utf-8') - else: - # instance is a scalar value - if index is not None: - # Must not have an index for array values - raise ValueError('index must be None') - if isinstance(self.__value[1], str): - # The value already is a string, return - return self.__value[1] - elif isinstance(self.__value[1], bytes): - # The value is a bytes object, attempt to decode - return self.__value[1].decode('utf-8') + if not isinstance(index, int): + # Index must be an int + raise ValueError('index must not be None') + # Type hint; access array element + v: Tuple[str, Union[bytes, str]] = self.__value[index] + if isinstance(v[1], str): + # The value already is a string, return + return v[1] + elif isinstance(v[1], bytes): + # The value is a bytes object, attempt to decode + return v[1].decode('utf-8') raise TypeError('Value is neither a str nor bytes') - def get_bytes(self, index: int = None) -> bytes: + def get_bytes(self, index: int = 0) -> bytes: """ - Attempts to return a value as a bytes object. If this instance is an scalar, no index must be provided. If - this instance is an array, an index must be provided. + Attempts to return a value as a bytes object. The index defaults to 0. - :param index: For array values: The index of the value to retrieve. For scalar values: None (default). + :param index: The index of the value to retrieve. Default: 0. :return: A bytes object representation of the requested value. Strings will be encoded as UTF-8. :raises IndexError: If the index is out of bounds. - :raises ValueError: If this is an array value, and no index is provided, or if this is a scalar value and an - index is provided. + :raises ValueError: If the index is not an int. :raises TypeError: If the requested value is neither a str nor a bytes object. """ - if self.is_array: - # instance is an array value - if index is None: - # Needs an index for array values - raise ValueError('index must not be None') - # Type hint; access array element - v: Tuple[str, Union[bytes, str]] = self.__value[index] - if isinstance(v[1], bytes): - # The value already is a bytes object, return - return v[1] - elif isinstance(v[1], str): - # The value is a string, encode first - return v[1].encode('utf-8') - else: - # instance is a scalar value - if index is not None: - # Must not have an index for array values - raise ValueError('index must be None') - if isinstance(self.__value[1], bytes): - # The value already is a bytes object, return - return self.__value[1] - elif isinstance(self.__value[1], str): - # The value is a string, encode first - return self.__value[1].encode('utf-8') + if not isinstance(index, int): + # Index must be a int + raise ValueError('index must not be None') + # Type hint; access array element + v: Tuple[str, Union[bytes, str]] = self.__value[index] + if isinstance(v[1], bytes): + # The value already is a bytes object, return + return v[1] + elif isinstance(v[1], str): + # The value is a string, encode first + return v[1].encode('utf-8') raise TypeError('Value is neither a str nor bytes') - def get_content_type(self, index: int = None) -> Optional[str]: + def get_content_type(self, index: int = 0) -> str: """ - Attempts to retrieve a value's Content-Type. If this instance is an scalar, no index must be provided. If this - instance is an array, an index must be provided. + Attempts to retrieve a value's Content-Type. The index defaults to 0. - :param index: For array values: The index of the value to retrieve. For scalar values: None (default). + :param index: The index of the value to retrieve. Default: 0. :return: The Content-Type of the requested value, as sent by the client. Not necessarily trustworthy. :raises IndexError: If the index is out of bounds. - :raises ValueError: If this is an array value, and no index is provided, or if this is a scalar value and an - index is provided. + :raises ValueError: If the index is not an int. """ - if self.is_array: - # instance is an array value - if index is None: - # Needs an index for array values - raise ValueError('index must not be None') - # Type hint; access array element - va: Tuple[str, Union[bytes, str]] = self.__value[index] - # Return the content type of the requested value - return va[0] - else: - # instance is a scalar value - if index is not None: - # Must not have an index for array values - raise ValueError('index must be None') - # Type hint - vs: Tuple[str, Union[bytes, str]] = self.__value - # Return the content type of the scalar value - return vs[0] + # instance is an array value + if not isinstance(index, int): + # Needs an index for array values + raise ValueError('index must not be None') + # Type hint; access array element + va: Tuple[str, Union[bytes, str]] = self.__value[index] + # Return the content type of the requested value + return va[0] def append(self, ctype: str, value: Union[str, bytes]): """ @@ -205,21 +159,13 @@ class RequestArgument(object): if self.is_view: # This is an immutable view, raise exception raise TypeError('A RequestArgument view is immutable!') - if len(self) == 0: - # Turn an empty argument into a scalar - self.__value = ctype, value - else: - # First turn the scalar into a one-element array ... - if self.is_scalar: - self.__value = [self.__value] - # ... then append the new value - self.__value.append((ctype, value)) + self.__value.append((ctype, value)) def __len__(self) -> int: """ :return: Number of values for this argument. """ - return len(self.__value) if self.is_array else 1 + return len(self.__value) def __iter__(self) -> Iterator['RequestArgument']: """ @@ -228,29 +174,18 @@ class RequestArgument(object): :return: An iterator that yields immutable views of the values. """ - if self.is_scalar: - # If this is a scalar, yield an immutable view of the single value - yield _View(self.__name, self.__value) - else: - # Typing helper - vs: List[Tuple[str, Union[bytes, str]]] = self.__value - for v in vs: - # If this is an array, yield an immutable scalar view for each (ctype, value) element in the array - yield _View(self.__name, v) + for v in self.__value: + # Yield an immutable scalar view for each (ctype, value) element in the array + yield _View(self.__name, v) def __getitem__(self, index: Union[int, slice]): """ Index the argument with either an int or a slice. The returned values are represented as immutable - RequestArgument views. Scalar arguments may be indexed with int(0). + RequestArgument views. + :param index: The index or slice. :return: An immutable view of the indexed elements of this argument. """ - if self.is_scalar: - # Scalars may only be indexed with 0 - if index == 0: - # Return an immutable view of the single scalar value - return _View(self.__name, self.__value) - raise IndexError('Scalar RequestArgument only indexable with 0') # Pass the index or slice through to the array, packing the result in an immutable view return _View(self.__name, self.__value[index]) diff --git a/matemat/webserver/test/test_requestargs.py b/matemat/webserver/test/test_requestargs.py index 133ea4a..3383863 100644 --- a/matemat/webserver/test/test_requestargs.py +++ b/matemat/webserver/test/test_requestargs.py @@ -43,13 +43,17 @@ class TestRequestArguments(unittest.TestCase): self.assertEqual(b'bar', ra.get_bytes()) # Content-Type must be set correctly self.assertEqual('text/plain', ra.get_content_type()) - # Using indices must result in an error - with self.assertRaises(ValueError): - self.assertEqual('bar', ra.get_str(0)) - with self.assertRaises(ValueError): - self.assertEqual('bar', ra.get_bytes(0)) - with self.assertRaises(ValueError): - self.assertEqual('bar', ra.get_content_type(0)) + # Using 0 indices must yield the same results + self.assertEqual('bar', ra.get_str(0)) + self.assertEqual(b'bar', ra.get_bytes(0)) + self.assertEqual('text/plain', ra.get_content_type(0)) + # Using other indices must result in an error + with self.assertRaises(IndexError): + ra.get_str(1) + with self.assertRaises(IndexError): + ra.get_bytes(1) + with self.assertRaises(IndexError): + ra.get_content_type(1) # Must not be a view self.assertFalse(ra.is_view) @@ -69,13 +73,17 @@ class TestRequestArguments(unittest.TestCase): self.assertEqual(b'bar', ra.get_bytes()) # Content-Type must be set correctly self.assertEqual('text/plain', ra.get_content_type()) - # Using indices must result in an error - with self.assertRaises(ValueError): - self.assertEqual('bar', ra.get_str(0)) - with self.assertRaises(ValueError): - self.assertEqual('bar', ra.get_bytes(0)) - with self.assertRaises(ValueError): - self.assertEqual('bar', ra.get_content_type(0)) + # Using 0 indices must yield the same results + self.assertEqual('bar', ra.get_str(0)) + self.assertEqual(b'bar', ra.get_bytes(0)) + self.assertEqual('text/plain', ra.get_content_type(0)) + # Using other indices must result in an error + with self.assertRaises(IndexError): + ra.get_str(1) + with self.assertRaises(IndexError): + ra.get_bytes(1) + with self.assertRaises(IndexError): + ra.get_content_type(1) # Must not be a view self.assertFalse(ra.is_view) @@ -96,13 +104,18 @@ class TestRequestArguments(unittest.TestCase): self.assertEqual(b'\x00\x80\xff\xfe', ra.get_bytes()) # Content-Type must be set correctly self.assertEqual('application/octet-stream', ra.get_content_type()) - # Using indices must result in an error - with self.assertRaises(ValueError): - self.assertEqual('bar', ra.get_str(0)) - with self.assertRaises(ValueError): - self.assertEqual('bar', ra.get_bytes(0)) - with self.assertRaises(ValueError): - self.assertEqual('bar', ra.get_content_type(0)) + # Using 0 indices must yield the same results + with self.assertRaises(UnicodeDecodeError): + ra.get_str(0) + self.assertEqual(b'\x00\x80\xff\xfe', ra.get_bytes(0)) + self.assertEqual('application/octet-stream', ra.get_content_type(0)) + # Using other indices must result in an error + with self.assertRaises(IndexError): + ra.get_str(1) + with self.assertRaises(IndexError): + ra.get_bytes(1) + with self.assertRaises(IndexError): + ra.get_content_type(1) # Must not be a view self.assertFalse(ra.is_view) @@ -120,13 +133,10 @@ class TestRequestArguments(unittest.TestCase): self.assertEqual(2, len(ra)) self.assertFalse(ra.is_scalar) self.assertTrue(ra.is_array) - # Retrieving values without an index must fail - with self.assertRaises(ValueError): - ra.get_str() - with self.assertRaises(ValueError): - ra.get_bytes() - with self.assertRaises(ValueError): - ra.get_content_type() + # Retrieving values without an index must yield the first element + self.assertEqual('bar', ra.get_str()) + self.assertEqual(b'bar', ra.get_bytes()) + self.assertEqual('text/plain', ra.get_content_type()) # The first value must be representable both as str and bytes, and have ctype text/plain self.assertEqual('bar', ra.get_str(0)) self.assertEqual(b'bar', ra.get_bytes(0))