Reworked RequestArgument API to somewhat more lax concerning 0-indices, potentially leading to safer code.
This commit is contained in:
parent
73c7dbe89f
commit
21a927046d
2 changed files with 94 additions and 149 deletions
|
@ -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:
|
|
||||||
# An array of length 1 will be reduced to a scalar
|
|
||||||
self.__value = value[0]
|
|
||||||
else:
|
|
||||||
# Store the array
|
# Store the array
|
||||||
self.__value = value
|
self.__value = value
|
||||||
else:
|
else:
|
||||||
# Scalar value, simply store
|
# Turn scalar into an array before storing
|
||||||
self.__value = value
|
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,23 +83,19 @@ 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:
|
|
||||||
# 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
|
||||||
v: Tuple[str, Union[bytes, str]] = self.__value[index]
|
v: Tuple[str, Union[bytes, str]] = self.__value[index]
|
||||||
|
@ -113,35 +105,20 @@ class RequestArgument(object):
|
||||||
elif isinstance(v[1], bytes):
|
elif isinstance(v[1], bytes):
|
||||||
# The value is a bytes object, attempt to decode
|
# The value is a bytes object, attempt to decode
|
||||||
return v[1].decode('utf-8')
|
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:
|
|
||||||
# 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
|
||||||
v: Tuple[str, Union[bytes, str]] = self.__value[index]
|
v: Tuple[str, Union[bytes, str]] = self.__value[index]
|
||||||
|
@ -151,48 +128,25 @@ class RequestArgument(object):
|
||||||
elif isinstance(v[1], str):
|
elif isinstance(v[1], str):
|
||||||
# The value is a string, encode first
|
# The value is a string, encode first
|
||||||
return v[1].encode('utf-8')
|
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 index is None:
|
if not isinstance(index, int):
|
||||||
# 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:
|
|
||||||
# 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:
|
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)
|
|
||||||
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)
|
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])
|
||||||
|
|
||||||
|
|
|
@ -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))
|
||||||
with self.assertRaises(ValueError):
|
self.assertEqual(b'bar', ra.get_bytes(0))
|
||||||
self.assertEqual('bar', ra.get_bytes(0))
|
self.assertEqual('text/plain', 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)
|
||||||
|
|
||||||
|
@ -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))
|
||||||
with self.assertRaises(ValueError):
|
self.assertEqual(b'bar', ra.get_bytes(0))
|
||||||
self.assertEqual('bar', ra.get_bytes(0))
|
self.assertEqual('text/plain', 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)
|
||||||
|
|
||||||
|
@ -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))
|
||||||
|
|
Loading…
Reference in a new issue