Ticket #416: autosite.py

File autosite.py, 11.5 KB (added by garthk, 10 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