Reworked RequestArgument API to somewhat more lax concerning 0-indices, potentially leading to safer code.

This commit is contained in:
s3lph 2018-06-29 18:29:51 +02:00
parent 73c7dbe89f
commit 21a927046d
2 changed files with 94 additions and 149 deletions

View file

@ -43,35 +43,31 @@ class RequestArgument(object):
# Assign name # Assign name
self.__name: str = name self.__name: str = name
# Initialize value # 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 # Default to empty array
if value is None: if value is None:
self.__value = [] self.__value = []
else: else:
if isinstance(value, list): if isinstance(value, list):
if len(value) == 1: # Store the array
# 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
self.__value = value self.__value = value
else:
# Turn scalar into an array before storing
self.__value = [value]
@property @property
def is_array(self) -> bool: def is_array(self) -> bool:
""" """
:return: True, if the value is a (possibly empty) array, False otherwise. :return: True, if the value is a (possibly empty) array, False otherwise.
""" """
return isinstance(self.__value, list) return len(self.__value) != 1
@property @property
def is_scalar(self) -> bool: def is_scalar(self) -> bool:
""" """
:return: True, if the value is a single scalar value, False otherwise. :return: True, if the value is a single scalar value, False otherwise.
""" """
return not isinstance(self.__value, list) return len(self.__value) == 1
@property @property
def is_view(self) -> bool: def is_view(self) -> bool:
@ -87,112 +83,70 @@ class RequestArgument(object):
""" """
return self.__name 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 Attempts to return a value as a string. The index defaults to 0.
instance is an array, an index must be provided.
: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. :return: An UTF-8 string representation of the requested value.
:raises UnicodeDecodeError: If the value cannot be decoded into an UTF-8 string. :raises UnicodeDecodeError: If the value cannot be decoded into an UTF-8 string.
:raises IndexError: If the index is out of bounds. :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 :raises ValueError: If the index is not an int.
index is provided.
:raises TypeError: If the requested value is neither a str nor a bytes object. :raises TypeError: If the requested value is neither a str nor a bytes object.
""" """
if self.is_array: if not isinstance(index, int):
# instance is an array value # Index must be an int
if index is None: raise ValueError('index must not be None')
# Needs an index for array values # Type hint; access array element
raise ValueError('index must not be None') v: Tuple[str, Union[bytes, str]] = self.__value[index]
# Type hint; access array element if isinstance(v[1], str):
v: Tuple[str, Union[bytes, str]] = self.__value[index] # The value already is a string, return
if isinstance(v[1], str): return v[1]
# The value already is a string, return elif isinstance(v[1], bytes):
return v[1] # The value is a bytes object, attempt to decode
elif isinstance(v[1], bytes): return v[1].decode('utf-8')
# 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')
raise TypeError('Value is neither a str nor bytes') 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 Attempts to return a value as a bytes object. The index defaults to 0.
this instance is an array, an index must be provided.
: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. :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 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 :raises ValueError: If the index is not an int.
index is provided.
:raises TypeError: If the requested value is neither a str nor a bytes object. :raises TypeError: If the requested value is neither a str nor a bytes object.
""" """
if self.is_array: if not isinstance(index, int):
# instance is an array value # Index must be a int
if index is None: raise ValueError('index must not be None')
# Needs an index for array values # Type hint; access array element
raise ValueError('index must not be None') v: Tuple[str, Union[bytes, str]] = self.__value[index]
# Type hint; access array element if isinstance(v[1], bytes):
v: Tuple[str, Union[bytes, str]] = self.__value[index] # The value already is a bytes object, return
if isinstance(v[1], bytes): return v[1]
# The value already is a bytes object, return elif isinstance(v[1], str):
return v[1] # The value is a string, encode first
elif isinstance(v[1], str): return v[1].encode('utf-8')
# 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')
raise TypeError('Value is neither a str nor bytes') 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 Attempts to retrieve a value's Content-Type. The index defaults to 0.
instance is an array, an index must be provided.
: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. :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 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 :raises ValueError: If the index is not an int.
index is provided.
""" """
if self.is_array: # instance is an array value
# instance is an array value if not isinstance(index, int):
if index is None: # Needs an index for array values
# Needs an index for array values raise ValueError('index must not be None')
raise ValueError('index must not be None') # Type hint; access array element
# Type hint; access array element va: Tuple[str, Union[bytes, str]] = self.__value[index]
va: Tuple[str, Union[bytes, str]] = self.__value[index] # Return the content type of the requested value
# Return the content type of the requested value return va[0]
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]
def append(self, ctype: str, value: Union[str, bytes]): def append(self, ctype: str, value: Union[str, bytes]):
""" """
@ -205,21 +159,13 @@ class RequestArgument(object):
if self.is_view: if self.is_view:
# This is an immutable view, raise exception # This is an immutable view, raise exception
raise TypeError('A RequestArgument view is immutable!') raise TypeError('A RequestArgument view is immutable!')
if len(self) == 0: self.__value.append((ctype, value))
# 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))
def __len__(self) -> int: def __len__(self) -> int:
""" """
:return: Number of values for this argument. :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']: def __iter__(self) -> Iterator['RequestArgument']:
""" """
@ -228,29 +174,18 @@ class RequestArgument(object):
:return: An iterator that yields immutable views of the values. :return: An iterator that yields immutable views of the values.
""" """
if self.is_scalar: for v in self.__value:
# If this is a scalar, yield an immutable view of the single value # Yield an immutable scalar view for each (ctype, value) element in the array
yield _View(self.__name, self.__value) yield _View(self.__name, v)
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)
def __getitem__(self, index: Union[int, slice]): def __getitem__(self, index: Union[int, slice]):
""" """
Index the argument with either an int or a slice. The returned values are represented as immutable 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. :param index: The index or slice.
:return: An immutable view of the indexed elements of this argument. :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 # Pass the index or slice through to the array, packing the result in an immutable view
return _View(self.__name, self.__value[index]) return _View(self.__name, self.__value[index])

View file

@ -43,13 +43,17 @@ class TestRequestArguments(unittest.TestCase):
self.assertEqual(b'bar', ra.get_bytes()) self.assertEqual(b'bar', ra.get_bytes())
# Content-Type must be set correctly # Content-Type must be set correctly
self.assertEqual('text/plain', ra.get_content_type()) self.assertEqual('text/plain', ra.get_content_type())
# Using indices must result in an error # Using 0 indices must yield the same results
with self.assertRaises(ValueError): self.assertEqual('bar', ra.get_str(0))
self.assertEqual('bar', ra.get_str(0)) self.assertEqual(b'bar', ra.get_bytes(0))
with self.assertRaises(ValueError): self.assertEqual('text/plain', ra.get_content_type(0))
self.assertEqual('bar', ra.get_bytes(0)) # Using other indices must result in an error
with self.assertRaises(ValueError): with self.assertRaises(IndexError):
self.assertEqual('bar', ra.get_content_type(0)) 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 # Must not be a view
self.assertFalse(ra.is_view) self.assertFalse(ra.is_view)
@ -69,13 +73,17 @@ class TestRequestArguments(unittest.TestCase):
self.assertEqual(b'bar', ra.get_bytes()) self.assertEqual(b'bar', ra.get_bytes())
# Content-Type must be set correctly # Content-Type must be set correctly
self.assertEqual('text/plain', ra.get_content_type()) self.assertEqual('text/plain', ra.get_content_type())
# Using indices must result in an error # Using 0 indices must yield the same results
with self.assertRaises(ValueError): self.assertEqual('bar', ra.get_str(0))
self.assertEqual('bar', ra.get_str(0)) self.assertEqual(b'bar', ra.get_bytes(0))
with self.assertRaises(ValueError): self.assertEqual('text/plain', ra.get_content_type(0))
self.assertEqual('bar', ra.get_bytes(0)) # Using other indices must result in an error
with self.assertRaises(ValueError): with self.assertRaises(IndexError):
self.assertEqual('bar', ra.get_content_type(0)) 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 # Must not be a view
self.assertFalse(ra.is_view) self.assertFalse(ra.is_view)
@ -96,13 +104,18 @@ class TestRequestArguments(unittest.TestCase):
self.assertEqual(b'\x00\x80\xff\xfe', ra.get_bytes()) self.assertEqual(b'\x00\x80\xff\xfe', ra.get_bytes())
# Content-Type must be set correctly # Content-Type must be set correctly
self.assertEqual('application/octet-stream', ra.get_content_type()) self.assertEqual('application/octet-stream', ra.get_content_type())
# Using indices must result in an error # Using 0 indices must yield the same results
with self.assertRaises(ValueError): with self.assertRaises(UnicodeDecodeError):
self.assertEqual('bar', ra.get_str(0)) ra.get_str(0)
with self.assertRaises(ValueError): self.assertEqual(b'\x00\x80\xff\xfe', ra.get_bytes(0))
self.assertEqual('bar', ra.get_bytes(0)) self.assertEqual('application/octet-stream', ra.get_content_type(0))
with self.assertRaises(ValueError): # Using other indices must result in an error
self.assertEqual('bar', ra.get_content_type(0)) 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 # Must not be a view
self.assertFalse(ra.is_view) self.assertFalse(ra.is_view)
@ -120,13 +133,10 @@ class TestRequestArguments(unittest.TestCase):
self.assertEqual(2, len(ra)) self.assertEqual(2, len(ra))
self.assertFalse(ra.is_scalar) self.assertFalse(ra.is_scalar)
self.assertTrue(ra.is_array) self.assertTrue(ra.is_array)
# Retrieving values without an index must fail # Retrieving values without an index must yield the first element
with self.assertRaises(ValueError): self.assertEqual('bar', ra.get_str())
ra.get_str() self.assertEqual(b'bar', ra.get_bytes())
with self.assertRaises(ValueError): self.assertEqual('text/plain', ra.get_content_type())
ra.get_bytes()
with self.assertRaises(ValueError):
ra.get_content_type()
# The first value must be representable both as str and bytes, and have ctype text/plain # 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('bar', ra.get_str(0))
self.assertEqual(b'bar', ra.get_bytes(0)) self.assertEqual(b'bar', ra.get_bytes(0))