Commits

Jason Moiron committed 8cdbadb

initial checkin

  • Participants

Comments (0)

Files changed (6)

+ Copyright (c) 2010, Jason Moiron & Jeremy Self
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation
+ files (the "Software"), to deal in the Software without
+ restriction, including without limitation the rights to use,
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the
+ Software is furnished to do so, subject to the following
+ conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ OTHER DEALINGS IN THE SOFTWARE.
+
+include LICENSE
+include README.rst

File README.rst

Empty file added.

File dselector.py

+#!/usr/bin/env python
+
+"""A Django "port" of Luke Arno's "Selector".
+
+To use, create an instance of Parser in your urls.py.  Parser has special
+implementations of the 'patterns' and 'url' functions that essentially
+parse pattern urls into regular expressions, and pass them to the builtin
+django functions.  Using pattern urls, your urls become more about _what_
+they are matching rather than about _how_ they are matching it:
+
+    patterns('foo.views',
+        (r'^/(?P<name>[a-zA-Z0-9\-]+)/(?P<foos>\d*.?\d+)/$', 'index', {}, 'foo-index')
+
+becomes, via patterns:
+
+    parser.patterns('foo.views', (r'/{name:slug}/{foos:num}/', 'index', {}, 'foo-index'))
+
+In addition to the builtin patterns (check `pattern_types`), you may also
+register additional patterns to an instantiated parser in your URLs module,
+or subclass Parser and add them to use throughout your application.
+
+You can optionally end a smart pattern with '!' if you do not want to have
+the trailing $ appended (for use in application entry points).  To end with
+a literal '!', use '!!'.
+
+DSelector is distributed under the MIT license.
+"""
+
+import re
+from django.conf.urls.defaults import url as django_url
+from django.core.urlresolvers import RegexURLPattern, RegexURLResolver
+
+import calendar
+
+__all__ = ['pattern_types', 'Parser']
+
+pattern_types = {
+    'word'      : r'\w+',
+    'alpha'     : r'[a-zA-Z]+',
+    'digits'    : r'\d+',
+    'number'    : r'\d*.?\d+',
+    'chunk'     : r'[^/^.]+',
+    'segment'   : r'[^/]+',
+    'any'       : r'.*',
+    # common url pieces
+    'year'      : r'\d{4}',
+    'month'     : r'(%s)' % '|'.join(calendar.month_abbr[1:]),
+    'day'       : r'\d{1,2}',
+    'slug'      : r'[a-zA-Z0-9\-]+',
+}
+
+pattern_re = re.compile(r'{(?P<name>\w+):?(?P<pattern>\w+)?}')
+re_template = r'(?P<%s>%s)'
+re_findstr  = r'{%(name)s:%(pattern)s}'
+
+class Parser:
+    def __init__(self, **extra_patterns):
+        self.pattern_types = pattern_types.copy()
+        for key, val in extra_patterns.iteritems():
+            self.pattern_types[key] = val
+
+    def register(self, name, regex):
+        """Registers a new pattern or overrides an old one."""
+        # sanity check
+        re.compile(regex)
+        self.pattern_types[name] = regex
+        return True
+
+    def parse_pattern(self, pat):
+        """Parses a pattern to a full regex. Surrounds pattern w/ ^ & $, and
+        replaces the embedded patterns with regexes."""
+        matches = [m.groupdict() for m in pattern_re.finditer(pat)]
+        for match in matches:
+            # {'pattern': p, 'name': n}
+            p, n = match['pattern'], match['name']
+            if p is None:
+                p = 'segment'
+                findstr = '{%s}' % n
+            else:
+                findstr = re_findstr % match
+            replacement = re_template % (n, self.pattern_types[p])
+            pat = pat.replace(findstr, replacement)
+        pat = ('^%s$' % pat) if not pat.endswith('!') else ('^%s' % pat[:-1])
+        return pat
+
+    def patterns(self, prefix, *args):
+        """A replacement 'patterns' that implements smart url groups."""
+        pattern_list = []
+        for t in args:
+            if isinstance(t, (list, tuple)):
+                t = self.url(prefix=prefix, *t)
+            elif isinstance(t, RegexURLPattern):
+                t.add_prefix(prefix)
+            pattern_list.append(t)
+        return pattern_list
+
+    def url(self, regex, view, kwargs=None, name=None, prefix=''):
+        """A replacement for 'url' that understands smart url groups."""
+        regex = self.parse_pattern(regex)
+        return django_url(regex, view, kwargs, name, prefix)
+
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import os
+from setuptools import setup
+#from distutils.core import setup
+
+version = '0.4'
+
+path = os.path.dirname(__file__)
+if not path: path = '.'
+readme = open(path + '/README.rst', 'r').read()
+
+setup(name='django-selector',
+      version=version,
+      description=readme.split('\n')[0],
+      long_description=readme,
+      classifiers=[
+          'Development Status :: 4 - Beta',
+          'Intended Audience :: Developers',
+          'License :: OSI Approved :: MIT License',
+          'Programming Language :: Python',
+          'Operating System :: OS Independent',
+          'Topic :: Internet :: WWW/HTTP :: Dynamic Content :: CGI Tools/Libraries',
+      ],
+      keywords='django urls',
+      author='Jason Moiron',
+      author_email='jmoiron@jmoiron.net',
+      url='http://dev.jmoiron.net/hg/django-selector/',
+      license='MIT',
+      #packages=['argot'],
+      #scripts=['bin/argot'],
+      # setuptools specific
+      zip_safe=False,
+      test_suite = "tests",
+      # i'm not sure how to get my virtualenv or setuptools to realize that
+      # there is a perfectly fine system-wide lxml library available; until
+      # i fix that, there won't be a "hard" requirement, even though lxml is
+      # 100% required for argot to function
+      install_requires=[] # , 'lxml'],
+)
+
+
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+"""Tests for django-selector."""
+
+import unittest
+import re
+import calendar
+
+import dselector
+
+class DselectorTest(unittest.TestCase):
+    def test_default_patterns(self):
+        """Test the default {foo} parser."""
+        parser = dselector.Parser()
+        pat = re.compile(parser.parse_pattern('/archives/{year}/{month}'))
+        match = pat.match('/archives/2005/04')
+        self.failUnless(match)
+        self.failUnless(match.groupdict()['year'] == '2005')
+        self.failUnless(match.groupdict()['month'] == '04')
+        match = pat.match('/archives/twothousandfive/ohfour')
+        self.failUnless(match)
+        self.failUnless(match.groupdict()['year'] == 'twothousandfive')
+        self.failUnless(match.groupdict()['month'] == 'ohfour')
+        match = pat.match('/archives/foo.bar/baz.bif!@#$%')
+        self.failUnless(match)
+        self.failUnless(match.groupdict()['year'] == 'foo.bar')
+        self.failUnless(match.groupdict()['month'] == 'baz.bif!@#$%')
+
+    def test_pattern_parser(self):
+        """Some simple tests that show the parsers will match the expected
+        URLs and fail on the unexpected ones."""
+        parser = dselector.Parser()
+        pat = re.compile(parser.parse_pattern('/archives/{year:digits}/{month:digits}'))
+        match = pat.match('/archives/2005/04')
+        self.failUnless(match)
+        self.failUnless(match.groupdict()['year'] == '2005')
+        self.failUnless(match.groupdict()['month'] == '04')
+        pat = re.compile(parser.parse_pattern('/archives/{year:year}/{month:month}'))
+        match = pat.match('/archives/2005/04')
+        self.failUnless(not match) # month is 
+        match = pat.match('/archives/2005/%s' % calendar.month_abbr[3])
+        self.failUnless(match)
+        self.failUnless(match.groupdict()['year'] == '2005')
+        self.failUnless(match.groupdict()['month'] == calendar.month_abbr[3])
+
+
+
+