-
Notifications
You must be signed in to change notification settings - Fork 3
/
hurl.py
195 lines (151 loc) · 5.53 KB
/
hurl.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
import re
from collections import Callable
from django.conf.urls import url, include, patterns as urls_patterns
from django.core.exceptions import ImproperlyConfigured
__all__ = 'Hurl', 'ViewSpec', 'v', 'patterns'
DEFAULT_MATCHER = 'slug'
DEFAULT_MATCHERS = {
'int': r'\d+',
'slug': r'[\w-]+',
'str': r'[^/]+',
}
PATH_SEPARATOR = '/'
PARAM_SEPARATOR = ':'
DEFAULT_MATCHER_KEY = '__default__'
PATTERN_RE = re.compile(r'<([\w:]+?)>')
NAMED_TEMPLATE = r'(?P<{name}>{matcher})'
ANON_TEMPLATE = r'({matcher})'
try:
string_type = basestring
except NameError:
string_type = str
def patterns(prefix, pattern_dict):
h = Hurl()
return h.patterns(prefix, pattern_dict)
def include(arg, namespace=None, app_name=None):
return ViewSpec(view=(arg, namespace, app_name))
class Hurl(object):
def __init__(self, name_prefix=''):
self.matchers = Matchers()
self.name_prefix = name_prefix
self.transcriber = PatternTranscriber(self.matchers)
def patterns(self, prefix, pattern_dict):
urls = self.urls(pattern_dict)
return urls_patterns(prefix, *urls)
def urls(self, pattern_dict):
return tuple(self._urls(pattern_dict))
def _urls(self, pattern_dict):
tree = build_tree(pattern_dict)
urls = tree.urls(self.transcriber)
for pattern, view_spec, view_params in urls:
pattern = finalize_pattern(pattern)
view = view_spec.view
kwargs = view_spec.view_kwargs
name = self._view_name(view_spec)
yield pattern, view, kwargs, name
def _view_name(self, view_spec):
name = view_name(view_spec.view, view_spec.name)
if name and self.name_prefix:
name = '{prefix}_{name}'.format(prefix=self.name_prefix, name=name)
return name
@property
def default_matcher(self):
return self.matchers.default_matcher_name
@default_matcher.setter
def default_matcher(self, value):
self.matchers.default_matcher_name = value
def include(self, arg, namespace=None, app_name=None):
return include(arg, namespace, app_name)
# internals
def build_tree(url_conf, pattern=''):
if isinstance(url_conf, (string_type, Callable)):
url_conf = ViewSpec(url_conf)
if isinstance(url_conf, ViewSpec):
return UrlLeaf(pattern, view=url_conf)
if isinstance(url_conf, dict):
url_conf = url_conf.items()
node = UrlNode(pattern)
for sub_pattern, sub_conf in url_conf:
child = build_tree(sub_conf, sub_pattern)
node.children.append(child)
return node
class ViewSpec(object):
def __init__(self, view, name=None, view_kwargs=None):
self.view = view
self.name = name
self.view_kwargs = view_kwargs
v = ViewSpec # convenient alias
class UrlNode(object):
def __init__(self, pattern):
self.pattern = pattern
self.children = []
def urls(self, transcribe):
pattern, params = transcribe(self.pattern)
for child in self.children:
child_urls = list(child.urls(transcribe))
for url in child_urls:
yield self.merge_child_url(pattern, params, url)
def merge_child_url(self, pattern, params, child_url):
sub_pattern, view_spec, sub_params = child_url
if not sub_pattern:
sub_pattern = pattern
elif pattern:
sub_pattern = PATH_SEPARATOR.join((pattern, sub_pattern))
sub_params = sub_params.copy()
sub_params.update(params)
return sub_pattern, view_spec, sub_params
class UrlLeaf(object):
def __init__(self, pattern, view):
self.pattern = pattern
self.view = view
def urls(self, transcribe):
pattern, params = transcribe(self.pattern)
yield pattern, self.view, params
def view_name(view, name=None):
if name:
return name
if isinstance(view, string_type):
return view.split('.')[-1]
if isinstance(view, Callable):
return getattr(view, '__name__')
def finalize_pattern(pattern):
if pattern == '':
return r'^$'
return r'^{url}/$'.format(url=pattern)
class PatternTranscriber(object):
def __init__(self, matchers):
self.matchers = matchers
self.params = None
def transcribe_pattern(self, pattern):
self.params = {}
transcribed = PATTERN_RE.sub(self.replace, pattern)
return transcribed, self.params
__call__ = transcribe_pattern
def replace(self, match):
param, type = self.split_param(match.group(1))
self.params[param] = type
matcher = self.matchers.matcher(param, type)
template = NAMED_TEMPLATE if param else ANON_TEMPLATE
return template.format(matcher=matcher, name=param)
def split_param(self, param_string):
if param_string.count(PARAM_SEPARATOR) > 1:
raise ImproperlyConfigured('Syntax error: %s' % param_string)
param, _, type = param_string.partition(':')
return param, type
class Matchers(dict):
def __init__(self):
self.default_matcher_name = DEFAULT_MATCHER
super(Matchers, self).__init__(DEFAULT_MATCHERS)
def default_matcher(self):
return self[self.default_matcher_name]
def matcher(self, param, type):
if type:
if type in self:
return self[type]
else:
raise ImproperlyConfigured('Matcher not known: %s' % type)
else:
if param in self:
return self[param]
else:
return self.default_matcher()