Skip to content

Instantly share code, notes, and snippets.

@Lukasa
Last active August 29, 2015 14:16
Show Gist options
  • Save Lukasa/d46acbe7ded37ea4492f to your computer and use it in GitHub Desktop.
Save Lukasa/d46acbe7ded37ea4492f to your computer and use it in GitHub Desktop.
A prototype Python header mapping.
import collections
class HTTPHeaderMap(collections.MutableMapping):
"""
A structure that contains HTTP headers.
HTTP headers are a curious beast. At the surface level they look roughly
like a name-value set, but in practice they have many variations that
make them tricky:
- duplicate keys are allowed
- keys are compared case-insensitively
- duplicate keys are isomorphic to comma-separated values, *except when
they aren't*!
- they logically contain a form of ordering
This data structure is an attempt to preserve all of that information
while being as user-friendly as possible.
"""
def __init__(self, *args, **kwargs):
# The meat of the structure. In practice, headers are an ordered list
# of tuples. This early version of the data structure simply uses this
# directly under the covers.
self._nvps = []
for arg in args:
self._nvps.extend(canonical_form(arg))
for header in kwargs.items():
self._nvps.extend(canonical_form(header))
def __getitem__(self, key):
"""
Unlike the dict __getitem__, this returns a list of items in the order
they were added.
"""
values = []
for k, v in self._nvps:
if _keys_equal(k, key):
values.append(v)
if not values:
raise KeyError()
return values
def __setitem__(self, key, value):
"""
Unlike the dict __setitem__, this appends to the list of NVPs. It also
splits out headers that can be split on the comma.
"""
self._nvps.extend(canonical_form((key, sub_val)))
def __delitem__(self, key):
"""
Sadly, __delitem__ is kind of stupid here, but the best we can do is
delete all headers with a given key. To correctly achieve the 'KeyError
on missing key' logic from dictionaries, we need to do this slowly.
"""
indices = []
for (i, (k, v)) in enumerate(self._nvps):
if _keys_equal(k, key):
indices.append(i)
if not indices:
raise KeyError()
for i in indices[::-1]:
self._nvps.pop(i)
def __iter__(self):
"""
This mapping iterates like the list of tuples it is.
"""
for pair in self._nvps:
yield pair
def __len__(self):
"""
The length of this mapping is the number of individual headers.
"""
return len(self._nvps)
def __contains__(self, key):
"""
If any header is present with this key, returns True.
"""
return any(_keys_equal(key, k) for k, _ in self._nvps)
def keys(self):
"""
Returns an iterable of the header keys in the mapping. This explicitly
does not filter duplicates, ensuring that it's the same length as
len().
"""
for n, _ in self._nvps:
yield n
def items(self):
"""
This mapping iterates like the list of tuples it is.
"""
for item in self:
yield item
def values(self):
"""
This is an almost nonsensical query on a header dictionary, but we
satisfy it in the exact same way we satisfy 'keys'.
"""
for _, v in self._nvps:
yield v
def get(self, name, default=None):
"""
Unlike the dict get, this returns a list of items in the order
they were added.
"""
try:
return self[name]
except KeyError:
return default
def __eq__(self, other):
return self._nvps == other._nvps
def __ne__(self, other):
return self._nvps != other._nvps
def canonical_form(k, v):
"""
Returns an iterable of key-value-pairs corresponding to the header in
canonical form. This means that the header is split on commas unless for
any reason it's a super-special snowflake (I'm looking at you Set-Cookie).
"""
SPECIAL_SNOWFLAKES = set('set-cookie', 'set-cookie2')
k = k.lower()
if k in SPECIAL_SNOWFLAKES:
yield k, v
else:
for sub_val in v.split(','):
yield k, sub_val
def _keys_equal(x, y):
"""
Returns 'True' if the two keys are equal by the laws of HTTP headers.
"""
return x.lower() == y.lower():
@shazow
Copy link

shazow commented Mar 5, 2015

nvps is a weird variable name, I couldn't figure out what it meant. I guess I'm more used to the term key-value pairs than name-value pairs.

Maybe call it items? Seems like the most canonical Python parlance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment