Ticket #416: autosite.py

File autosite.py, 11.5 KB (added by garthk, 19 years ago)

AUTOSITE! Use it TODAY!

Line 
1import sys, os
2from django.conf.urls import defaults
3
4"""
5Automatic site configuration.
6
7In your settings module::
8
9 import autosite
10 ROOT_URLCONF = autosite.root_urlconf('sitename')
11 TEMPLATE_DIRS = autosite.template_dirs('sitename')
12 INSTALLED_APPS = autosite.installed_apps('sitename')
13
14In your site-level url configuration module, which thanks to `root_urlconf`
15can be in ``sitename/urls.py``
16rather than ``sitename/settings/urls/main.py``, you can use `sitepatterns`
17rather than `patterns` to do most of the work for you::
18
19 from autosite import sitepatterns
20 urlpatterns = patterns('', 'sitename',
21 (r'^/?$', 'sitename.views.root'),
22 # ... any more explicit site-level patterns and destinations...
23 )
24
25`sitepatterns` will iterate through the application packages looking
26for app-level url configuration modules (either ``appname/urls.py`` or
27``appname/urls/app.py``) and automatically add them to the site's pattern
28list with the pattern ``r'^appname/'``.
29
30If a app-level url configuration module can't be found, `sitepatterns`
31will iterate through ``sitename.apps.appname.views`` (whether it's a module
32or a package) looking for callables with a ``.urlpattern`` attribute
33(for which the value should be a regular expression) or a ``.urlpatterns``
34attribute (for which the value should be a list of regular expressions).
35
36For Python 2.3, you should set ``.urlpattern`` manually. For Python 2.4,
37the ``urlpattern`` decorator will do it for you::
38
39 @urlpattern(r'^/?$')
40 def index(request):
41 # ...
42
43Note that the view modules' expressions will be added in the alphabetic
44order of their function names. If you were depending on careful ordering
45of your pattern list, either keep using your manual url configuration
46module or use the ``(?<!...)`` negative lookbehind assertion.
47
48The practical upshot of using autosite is that you type less and don't
49have to maintain quite so many files. Rather than::
50
51 sitename/settings/__init__.py
52 sitename/settings/main.py
53 sitename/settings/admin.py
54 sitename/settings/urls/main.py
55 sitename/apps/appname/urls/__init__.py
56 sitename/apps/appname/urls/appname.py
57 sitename/apps/appname/views/__init__.py
58 sitename/apps/appname/views/appname.py
59
60... you can consolidate back to:
61
62 sitename/settings.py
63 sitename/urls.py
64 sitename/apps/appname/views.py
65
66This module has been brought to you by one programmer's bizarre tendency to
67to spend two hours writing 300+ lines of code to replace around 30 lines of
68code that were taking him less than two minutes per day to maintain. His
69insanity is your gain. If only 100 Django programmers benefit from this
70module, all his hard work will have been worthwhile.
71"""
72
73__all__ = [
74 'urlconf',
75 'patterns',
76 'template_dirs',
77 'installed_apps',
78 'sitepatterns',
79 'apppatterns',
80 'urlpattern',
81 ]
82
83def splitall(path):
84 """Split `path` into all of its components."""
85 segments = []
86 base = path
87 while True:
88 base, name = os.path.split(base)
89 if name:
90 segments.append(name)
91 else:
92 if base:
93 segments.append(base)
94 segments.reverse()
95 return segments
96
97def child_packages_and_modules(
98 packagename,
99 relative=True,
100 onlypackages=False,
101 returndirs=False,
102 depth=0):
103 """Return a list of packages and modules under `packagename`.
104
105 relative -- exclude the package name itself from the results
106 onlypackages -- exclude modules (*.py) from the results
107 returndirs -- return the path, not the module/package name
108 depth -- if not 0, limit the result depth
109 """
110
111 # Find the package
112 modstack = packagename.split('.')
113 package = __import__(packagename, {}, {}, modstack[-1])
114 packagedir, initfile = os.path.split(package.__file__)
115 packagedir = os.path.abspath(packagedir)
116 assert initfile in [ '__init__.py', '__init__.pyc' ]
117 assert os.path.isdir(packagedir)
118 packagedirstack = splitall(packagedir)
119 lpds = len(packagedirstack)
120 results = []
121
122 # Define a helpful 'append' method so we don't have to duplicate
123 # this logic.
124 def append(segments):
125 if depth and len(segments) > depth:
126 return
127 if returndirs:
128 joiner = lambda l: os.path.join(*l)
129 basesegments = packagedirstack
130 else:
131 joiner = '.'.join
132 basesegments = modstack
133 if relative and not returndirs:
134 if segments:
135 results.append(joiner(segments))
136 else:
137 results.append(joiner(basesegments + segments))
138
139 # Walk the package directory, gathering results.
140 for dirpath, dirnames, filenames in os.walk(packagedir):
141 dirstack = splitall(dirpath)
142 assert dirstack[:lpds] == packagedirstack
143 relsegments = dirstack[lpds:]
144 heremodstack = modstack + relsegments
145 heredirstack = packagedirstack + relsegments
146 if not ('__init__.py' in filenames or '__init__.pyc' in filenames):
147 # wherever we are, it isn't a package and we shouldn't
148 # recurse any deeper
149 del dirnames[:]
150 else:
151 append(relsegments)
152 if not onlypackages:
153 for filename in filenames:
154 name, ext = os.path.splitext(filename)
155 if ext == '.py' and name != '__init__':
156 append(relsegments + [name])
157 return results
158
159def urlconf(modulename, lookfor=None, verify=False):
160 """Find the urlconf for `modulename`, which should be the name of the
161 site or one of its applications (``"sitename.appname"``).
162
163 lookfor -- list of submodules to look for
164 """
165
166 if lookfor is None:
167 segments = modulename.split('.')
168 if 'apps' in segments:
169 # sitename.apps.appname
170 lookfor = ['urls.%s' % segments[-1], 'urls']
171 else:
172 # sitename
173 lookfor = ['settings.urls.main', 'urls']
174 submodules = child_packages_and_modules(modulename)
175 fails = []
176 for submodule in lookfor:
177 modname = '%s.%s' % (modulename, submodule)
178 if submodule in submodules:
179 if verify:
180 module = __import__(modname, {}, {}, 'urlconf_module')
181 if hasattr(module, 'urlpatterns'):
182 return modname
183 else:
184 raise AssertionError, "We thought %s was a urlconf " \
185 "module, but couldn't find urlpatterns in it." % (
186 modname)
187 else:
188 return modname
189 else:
190 fails.append(modname)
191 raise AssertionError, "Couldn't find a url module amongst %s." % (
192 ', '.join(fails))
193root_urlconf = urlconf
194
195def template_dirs(sitemodulename, lookfor=['templates'], incadmin=True):
196 """Find all the template/ directories in `sitemodulename`.
197
198 lookfor -- adjust what subdirectory names to look for
199 incadmin -- if True (default), include the administration templates."""
200
201 results = []
202 for packagedir in child_packages_and_modules(sitemodulename,
203 onlypackages=True, returndirs=True):
204 for subdir in lookfor:
205 templatedir = os.path.join(packagedir, subdir)
206 if os.path.isdir(templatedir):
207 results.append(templatedir)
208 if incadmin:
209 return results + template_dirs('django.conf',
210 lookfor=['admin_templates'],
211 incadmin=False)
212 return results
213
214def installed_apps(sitemodulename):
215 """Find all the apps in `sitemodulename`."""
216 return child_packages_and_modules('%s.apps' % sitemodulename,
217 onlypackages=True, depth=1, relative=False)[1:]
218
219def sitepatterns(basename, sitename, incadmin=True, *patlist):
220 """Like django.conf.urls.defaults.patterns, but automatically includes
221 the urlconfs for any applications it can find.
222
223 basename -- hopefully, used only for patlist
224 sitename -- the name of the site module to scan for apps and urlconfs
225 incadmin -- include the administration site.
226 """
227
228 patlist = list(patlist)
229 for appmodname in installed_apps(sitename):
230 appname = appmodname.split('.')[-1]
231 try:
232 uc = urlconf(appmodname)
233 pattern = (
234 r'^%s/' % appname,
235 defaults.include(urlconf(appmodname))
236 )
237 patlist.append(pattern)
238 except AssertionError: # Couldn't find it!
239 for urlpattern, target in _apppatterns(appmodname):
240 if urlpattern[:1] == '^':
241 urlpattern = urlpattern[1:]
242 pattern = (
243 r'^%s/%s' % (appname, urlpattern),
244 target
245 )
246 patlist.append(pattern)
247 if incadmin:
248 pattern = (r'^admin/', defaults.include('django.conf.urls.admin'))
249 patlist.append(pattern)
250 import pprint
251 pprint.pprint(patlist)
252 return defaults.patterns(basename, *patlist)
253
254def _apppatterns(appmodname):
255 """Does the heavy lifting for `apppatterns`."""
256 patlist = []
257 for modname in child_packages_and_modules(appmodname):
258 if modname == 'views' or modname.startswith('views.'):
259 fullmodname = '%s.%s' % (appmodname, modname)
260 module = __import__(fullmodname, {}, {}, fullmodname)
261 for attname in dir(module):
262 att = getattr(module, attname)
263 if callable(att):
264 if hasattr(att, 'urlpattern'):
265 pattern = (
266 getattr(att, 'urlpattern'),
267 '%s.%s' % (fullmodname, attname)
268 )
269 patlist.append(pattern)
270 elif hasattr(att, 'urlpatterns'):
271 for urlpattern in getattr(att, 'urlpatterns'):
272 pattern = (
273 urlpattern,
274 '%s.%s' % (fullmodname, attname)
275 )
276 patlist.append(pattern)
277 return patlist
278
279def apppatterns(basename, appmodname, *patlist):
280 """Like django.conf.urls.defaults.patterns, but looks for views modules
281 and scans them for view modules with `urlpattern` attributes.
282
283 basename -- only used for patlist
284 appmodname -- the name of the app in ``sitename.apps.appname`` format.
285 """
286
287 if basename:
288 # Incorporate basename so we don't have to pass it down.
289 patlist = [(urlpattern, '%s.%s' % (basename, target))
290 for urlpattern, target in patlist[:]]
291 else:
292 # Just make a list of it.
293 patlist = list(patlist)
294 patlist.extend(_apppatterns(appmodname))
295 return defaults.patterns('', *patlist)
296
297def urlpattern(urlp):
298 """Decorate a view function with a URL pattern to be picked up by
299 the automatic stuff above."""
300 def decorator(func):
301 if hasattr(func, 'urlpatterns'):
302 func.urlpatterns.append(urlp)
303 else:
304 func.urlpatterns = [urlp]
305 return func
306 return decorator
Back to Top