Skip to content

Instantly share code, notes, and snippets.

@NickSto
Created April 11, 2018 22:45
Show Gist options
  • Save NickSto/770bfadb95d006bd3008625ef3780273 to your computer and use it in GitHub Desktop.
Save NickSto/770bfadb95d006bd3008625ef3780273 to your computer and use it in GitHub Desktop.
A nice helper object to deal with query strings.
class QueryParams(collections.OrderedDict):
"""A class to simplify handling query parameters and enable simpler query strings.
This encapsulates parsing the values of query strings, holding their values, and producing
altered query strings in a simplified format.
It works by first declaring the accepted ("canonical") query string parameters with add().
This object retains information on the preferred order of parameters, along with their default
values and type.
Then, you give parse() the parameters given by the user, and it will interpret the values
according to their expected type.
Then, you can continue using this object as a dict holding the parameter values, and getting and
setting them as you wish.
Finally, you can produce query strings by giving this object to str(). The query strings will list
the parameters in the preferred order, omitting parameters with the default values."""
def __init__(self):
self.params = collections.OrderedDict()
self.invalid_value = False
def parse(self, params_dict):
"""Convenience function to set all the parameters at once.
Intended to be used like:
query_params.parse(request.GET)"""
for param_name, value in params_dict.items():
self.set(param_name, value)
def add(self, param_name, default=None, type=lambda x: x, min=None, max=None, choices=None):
"""Add a canonical parameter, set its default value and type.
The type should be a callable. Incoming values will be passed through the callable before
storing. If it throws a TypeError or ValueError, the default will be stored instead."""
param = QueryParam(name=param_name, default=default, type=type, min=min, max=max, choices=choices)
self.params[param_name] = param
self[param_name] = default
def set(self, param_name, value):
"""Set the value of a parameter.
The value will be converted into the parameter's type.
It doesn't have to be a canonical one. In that case, its order will be after all current
canonical ones."""
param = self.params.get(param_name, QueryParam(param_name))
if param_name not in self.params:
self.params[param_name] = param
try:
parsed_value = param.type(value)
except (TypeError, ValueError):
# If it's an invalid value, set it to be the default.
parsed_value = param.default
self.invalid_value = True
if param.min is not None and parsed_value < param.min:
parsed_value = param.min
self.invalid_value = True
if param.max is not None and parsed_value > param.max:
parsed_value = param.max
self.invalid_value = True
if param.choices is not None and parsed_value not in param.choices:
parsed_value = param.default
self.invalid_value = True
self[param_name] = parsed_value
def but_with(self, *args, **kwargs):
"""Return a copy of the current object, but with the given parameter(s) set to the given value.
Either give keyword arguments, or positional arguments with alternating parameter names & values.
Usage:
params.but_with(p=1, user='me')
params.but_with('p', 1, 'user', 'me')
"""
new_query_params = self.copy()
for i in range(0, len(args), 2):
if len(args) < i+2:
break
param_name = args[i]
value = args[i+1]
new_query_params.set(param_name, value)
for param_name, value in kwargs.items():
new_query_params.set(param_name, value)
return new_query_params
def copy(self):
copy = super().copy()
copy.params = collections.OrderedDict()
for param_name, param in self.params.items():
copy.params[param_name] = param.copy()
return copy
def __str__(self):
"""Return a query string with the parameters set to their current values.
Preserves the canonical order of the parameters, and omits any whose current value is the default.
"""
components = []
for param_name, value in self.items():
param = self.params.get(param_name, QueryParam(param_name))
if value == param.default:
continue
param_name_quoted = urllib.parse.quote(param_name)
value_quoted = urllib.parse.quote(str(value))
components.append('{}={}'.format(param_name_quoted, value_quoted))
if components:
return '?'+'&'.join(components)
else:
return ''
class QueryParam(object):
def __init__(self, name, default=None, type=lambda x: x, min=None, max=None, choices=None):
self.name = name
self.default = default
self.type = type
self.min = min
self.max = max
self.choices = choices
def copy(self):
return QueryParam(self.name, default=self.default, type=self.type, min=self.min, max=self.max)
def boolish(value):
if value in (False, None, 0, '', 'False', 'false', '0'):
return False
elif value in (True, 1, 'True', 'true', '1'):
return True
else:
raise ValueError('Invalid boolish value {!r}.'.format(value))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment