Skip to content

Instantly share code, notes, and snippets.

@shiweifu
Created April 13, 2013 03:52
Show Gist options
  • Save shiweifu/5376846 to your computer and use it in GitHub Desktop.
Save shiweifu/5376846 to your computer and use it in GitHub Desktop.
#!/usr/bin/python
#coding: utf-8
import re
class MappingFileError(Exception): pass
class SimpleParser(object):
"""Callable to turn path expressions into regexes with named groups.
For instance "/hello/{name}" becomes r"^\/hello\/(?P<name>[^\^.]+)$"
For /hello/{name:pattern}
you get whatever is in self.patterns['pattern'] instead of "[^\^.]+"
Optional portions of path expression can be expressed [like this]
/hello/{name}[/] (can have trailing slash or not)
Example:
/blog/archive/{year:digits}/{month:digits}[/[{article}[/]]]
This would catch any of these:
/blog/archive/2005/09
/blog/archive/2005/09/
/blog/archive/2005/09/1
/blog/archive/2005/09/1/
(I am not suggesting that this example is a best practice.
I would probably have a separate mapping for listing the month
and retrieving an individual entry. It depends, though.)
"""
start, end = '{}'
ostart, oend = '[]'
_patterns = {'word': r'\w+',
'alpha': r'[a-zA-Z]+',
'digits': r'\d+',
'number': r'\d*.?\d+',
'chunk': r'[^/^.]+',
'segment': r'[^/]+',
'any': r'.+'}
default_pattern = 'chunk'
def __init__(self, patterns=None):
"""Initialize with character class mappings."""
self.patterns = dict(self._patterns)
if patterns is not None:
self.patterns.update(patterns)
def lookup(self, name):
"""Return the replacement for the name found."""
if ':' in name:
name, pattern = name.split(':')
pattern = self.patterns[pattern]
else:
pattern = self.patterns[self.default_pattern]
if name == '':
name = '__pos%s' % self._pos
self._pos += 1
return '(?P<%s>%s)' % (name, pattern)
def lastly(self, regex):
"""Process the result of __call__ right before it returns.
Adds the ^ and the $ to the beginning and the end, respectively.
"""
return "^%s$" % regex
def openended(self, regex):
"""Process the result of __call__ right before it returns.
Adds the ^ to the beginning but no $ to the end.
Called as a special alternative to lastly.
"""
return "^%s" % regex
def outermost_optionals_split(self, text):
"""Split out optional portions by outermost matching delims."""
parts = []
buffer = ""
starts = ends = 0
for c in text:
if c == self.ostart:
if starts == 0:
parts.append(buffer)
buffer = ""
else:
buffer += c
starts +=1
elif c == self.oend:
ends +=1
if starts == ends:
parts.append(buffer)
buffer = ""
starts = ends = 0
else:
buffer += c
else:
buffer += c
if not starts == ends == 0:
raise PathExpressionParserError(
"Mismatch of optional portion delimiters."
)
parts.append(buffer)
return parts
def parse(self, text):
"""Turn a path expression into regex."""
if self.ostart in text:
parts = self.outermost_optionals_split(text)
parts = map(self.parse, parts)
parts[1::2] = ["(%s)?" % p for p in parts[1::2]]
else:
parts = [part.split(self.end)
for part in text.split(self.start)]
parts = [y for x in parts for y in x]
parts[::2] = map(re.escape, parts[::2])
parts[1::2] = map(self.lookup, parts[1::2])
return ''.join(parts)
def __call__(self, url_pattern):
"""Turn a path expression into regex via parse and lastly."""
self._pos = 0
if url_pattern.endswith('|'):
return self.openended(self.parse(url_pattern[:-1]))
else:
return self.lastly(self.parse(url_pattern))
class Application(object):
route_list = []
def __init__(self):
super(Application, self).__init__()
self.parser = SimpleParser()
def route(self, path):
def wrapper(callback):
# print path
regex = self.parser(path)
print regex
regex = re.compile(regex)
self.route_list.append((regex, callback))
return wrapper
def __call__(self, url):
for l in self.route_list:
m = l[0].search(url)
if m:
d = m.groupdict()
return l[1](**d)
return "404"
app = Application()
@app.route('/view/{page}[/{revision}]')
def index(page, revision=None):
print page
return "hello world"
print app("/view/abc")
def main():
parser = SimpleParser()
regex = parser("/blog/archive/{year:digits}/{month:digits}[/[{article}[/]]]")
compiled_regex = re.compile(regex)
m = compiled_regex.match("/blog/archive/abcd/aaa/abcdefg/")
# print m.groups()
# mappings = []
# mappings.append((compiled_regex, {}))
# print mappings
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment