Documentation of the RequestArgument class.

This commit is contained in:
s3lph 2018-06-29 01:12:25 +02:00
parent 118de8bf95
commit 8898abc77b
2 changed files with 159 additions and 6 deletions

View file

@ -1,121 +1,273 @@
from typing import List, Optional, Tuple, Union from typing import Iterator, List, Optional, Tuple, Union
class RequestArgument(object): class RequestArgument(object):
"""
Container class for HTTP request arguments that simplifies dealing with
- scalar and array arguments:
Automatically converts between single values and arrays where necessary: Arrays with one element can be
accessed as scalars, and scalars can be iterated, yielding themselves as a single item.
- UTF-8 strings and binary data (e.g. file uploads):
All data can be retrieved both as a str (if utf-8 decoding is possible) and a bytes object.
The objects returned from iteration or indexing are immutable views of (parts of) this object.
Usage example:
qsargs = urllib.parse.parse_qs(qs, strict_parsing=True, keep_blank_values=True, errors='strict')
args: Dict[str, RequestArgument] = dict()
for k, vs in qsargs:
args[k] = RequestArgument(k)
for v in vs:
# text/plain usually is a sensible choice for values decoded from urlencoded strings
# IF ALREADY IN STRING FORM (which parse_qs does)!
args[k].append('text/plain', v)
if 'username' in args and args['username'].is_scalar:
username: str = args['username'].get_str()
else:
raise ValueError()
"""
def __init__(self, def __init__(self,
name: str, name: str,
value: Union[Tuple[str, Union[bytes, str]], List[Tuple[str, Union[bytes, str]]]] = None) -> None: value: Union[Tuple[str, Union[bytes, str]], List[Tuple[str, Union[bytes, str]]]] = None) -> None:
"""
Create a new RequestArgument with a name and optionally an initial value.
:param name: The name for this argument, as provided via GET or POST.
:param value: The initial value, if any. Optional, initializes with empty array if omitted.
"""
# Assign name
self.__name: str = name self.__name: str = name
# Initialize value
self.__value: Union[Tuple[str, Union[bytes, str]], List[Tuple[str, Union[bytes, str]]]] = None self.__value: Union[Tuple[str, Union[bytes, str]], List[Tuple[str, Union[bytes, str]]]] = None
# 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: if len(value) == 1:
# An array of length 1 will be reduced to a scalar
self.__value = value[0] self.__value = value[0]
else: else:
# Store the array
self.__value = value self.__value = value
else: else:
# Scalar value, simply store
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 isinstance(self.__value, list) return isinstance(self.__value, list)
@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 not isinstance(self.__value, list) return not isinstance(self.__value, list)
@property @property
def is_view(self) -> bool: def is_view(self) -> bool:
"""
:return: True, if this instance is an immutable view, False otherwise.
"""
return False return False
@property @property
def name(self) -> str: def name(self) -> str:
"""
:return: The name of this argument.
"""
return self.__name return self.__name
def get_str(self, index: int = None) -> Optional[str]: def get_str(self, index: int = None) -> 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.
:param index: For array values: The index of the value to retrieve. For scalar values: None (default).
: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.
"""
if self.is_array: if self.is_array:
# instance is an array value
if index is None: 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
v: Tuple[str, Union[bytes, str]] = self.__value[index] v: Tuple[str, Union[bytes, str]] = self.__value[index]
if isinstance(v[1], str): if isinstance(v[1], str):
# The value already is a string, return
return v[1] return v[1]
elif isinstance(v[1], bytes): elif isinstance(v[1], bytes):
# The value is a bytes object, attempt to decode
return v[1].decode('utf-8') return v[1].decode('utf-8')
else: else:
# instance is a scalar value
if index is not None: if index is not None:
# Must not have an index for array values
raise ValueError('index must be None') raise ValueError('index must be None')
if isinstance(self.__value[1], str): if isinstance(self.__value[1], str):
# The value already is a string, return
return self.__value[1] return self.__value[1]
elif isinstance(self.__value[1], bytes): elif isinstance(self.__value[1], bytes):
# The value is a bytes object, attempt to decode
return self.__value[1].decode('utf-8') return self.__value[1].decode('utf-8')
def get_bytes(self, index: int = None) -> Optional[bytes]: def get_bytes(self, index: int = None) -> 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.
:param index: For array values: The index of the value to retrieve. For scalar values: None (default).
: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.
"""
if self.is_array: if self.is_array:
# instance is an array value
if index is None: 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
v: Tuple[str, Union[bytes, str]] = self.__value[index] v: Tuple[str, Union[bytes, str]] = self.__value[index]
if isinstance(v[1], bytes): if isinstance(v[1], bytes):
# The value already is a bytes object, return
return v[1] return v[1]
elif isinstance(v[1], str): elif isinstance(v[1], str):
# The value is a string, encode first
return v[1].encode('utf-8') return v[1].encode('utf-8')
else: else:
# instance is a scalar value
if index is not None: if index is not None:
# Must not have an index for array values
raise ValueError('index must be None') raise ValueError('index must be None')
if isinstance(self.__value[1], bytes): if isinstance(self.__value[1], bytes):
# The value already is a bytes object, return
return self.__value[1] return self.__value[1]
elif isinstance(self.__value[1], str): elif isinstance(self.__value[1], str):
# The value is a string, encode first
return self.__value[1].encode('utf-8') return self.__value[1].encode('utf-8')
def get_content_type(self, index: int = None) -> Optional[str]: def get_content_type(self, index: int = None) -> Optional[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.
:param index: For array values: The index of the value to retrieve. For scalar values: None (default).
: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.
"""
if self.is_array: if self.is_array:
# instance is an array value
if index is None: 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
v: Tuple[str, Union[bytes, str]] = self.__value[index] v: Tuple[str, Union[bytes, str]] = self.__value[index]
# Return the content type of the requested value
return v[0] return v[0]
else: else:
# instance is a scalar value
if index is not None: if index is not None:
# Must not have an index for array values
raise ValueError('index must be None') raise ValueError('index must be None')
# Return the content type of the scalar value
return self.__value[0] return self.__value[0]
def append(self, ctype: str, value: Union[str, bytes]): def append(self, ctype: str, value: Union[str, bytes]):
"""
Append a value to this instance. Turns an empty argument into a scalar and a scalar into an array.
:param ctype: The Content-Type, as provided in the request.
:param value: The scalar value to append, either a string or bytes object.
:raises TypeError: If called on an immutable view.
"""
if self.is_view: if self.is_view:
# 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: if len(self) == 0:
# Turn an empty argument into a scalar
self.__value = ctype, value self.__value = ctype, value
else: else:
# First turn the scalar into a one-element array ...
if self.is_scalar: if self.is_scalar:
self.__value = [self.__value] self.__value = [self.__value]
# ... then append the new value
self.__value.append((ctype, value)) self.__value.append((ctype, value))
def __len__(self): 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) if self.is_array else 1
def __iter__(self): def __iter__(self) -> Iterator['RequestArgument']:
"""
Iterate the values of this argument. Each value is accessible as if it were a scalar RequestArgument in turn,
although they are immutable.
:return: An iterator that yields immutable views of the values.
"""
if self.is_scalar: if self.is_scalar:
# If this is a scalar, yield an immutable view of the single value
yield _View(self.__name, self.__value) yield _View(self.__name, self.__value)
else: else:
# Typing helper # Typing helper
_value: List[Tuple[str, Union[bytes, str]]] = self.__value _value: List[Tuple[str, Union[bytes, str]]] = self.__value
for v in _value: for v in _value:
# 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
RequestArgument views. Scalar arguments may be indexed with int(0).
:param index: The index or slice.
:return: An immutable view of the indexed elements of this argument.
"""
if self.is_scalar: if self.is_scalar:
# Scalars may only be indexed with 0
if index == 0: if index == 0:
# Return an immutable view of the single scalar value
return _View(self.__name, self.__value) return _View(self.__name, self.__value)
raise ValueError('Scalar RequestArgument only indexable with 0') raise ValueError('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]) return _View(self.__name, self.__value[index])
class _View(RequestArgument): class _View(RequestArgument):
"""
This class represents an immutable view of a (subset of a) RequestArgument object. Should not be instantiated
directly.
"""
def __init__(self, name: str, value: Union[Tuple[str, Union[bytes, str]], List[Tuple[str, Union[bytes, str]]]]): def __init__(self, name: str, value: Union[Tuple[str, Union[bytes, str]], List[Tuple[str, Union[bytes, str]]]])\
-> None:
"""
Create a new immutable view of a (subset of a) RequestArgument.
:param name: The name for this argument, same as in the original RequestArgument.
:param value: The values to represent in this view, obtained by e.g. indexing or slicing.
"""
super().__init__(name, value) super().__init__(name, value)
@property @property
def is_view(self) -> bool: def is_view(self) -> bool:
"""
:return: True, if this instance is an immutable view, False otherwise.
"""
return True return True

View file

@ -69,6 +69,7 @@ def _parse_multipart(body: bytes, boundary: str) -> List[RequestArgument]:
args[name] = RequestArgument(name) args[name] = RequestArgument(name)
args[name].append(hdr['Content-Type'].strip(), part) args[name].append(hdr['Content-Type'].strip(), part)
if not has_name: if not has_name:
# Content-Disposition header without name attribute
raise ValueError('mutlipart/form-data part without name attribute') raise ValueError('mutlipart/form-data part without name attribute')
return list(args.values()) return list(args.values())