Skip to content

Commit 243dbab

Browse files
author
Alexandr Shurigin
committed
0.1.0 Version initial commit.
0 parents  commit 243dbab

File tree

6 files changed

+334
-0
lines changed

6 files changed

+334
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
*.egg-info/
2+
.idea/

README.markdown

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# [Django Macros Url](https://github.com/phpdude/django-macros-url/) v0.1.0 - Routing must be simple as possible
2+
3+
Django Macros Url makes it easy to write (and read) url patterns in your django applications by using macros.
4+
5+
You can combine your prefixes with macro names with underscope, for example you can use macro `:slug` and `:product_slug`. They both will be compiled to same regex pattern with their group names of course. Multiple underscopes accepted too.
6+
7+
### Supported macros by default
8+
9+
```
10+
slug - [\w-]+
11+
year - \d{4}
12+
month - (0?([1-9])|10|11|12)
13+
day - ((0|1|2)?([1-9])|[1-3]0|31)
14+
id - \d+
15+
uuid - [a-fA-F0-9]{8}-?[a-fA-F0-9]{4}-?[a-fA-F0-9]{4}-?[a-fA-F0-9]{4}-?[a-fA-F0-9]{12}
16+
```
17+
18+
If you want to offer more macros by default, you can fork and make pull request.
19+
20+
### Usage
21+
22+
Django Macros Urls used same way as django standart urls. You just import this and declare your patterns with macros.
23+
24+
Also you can register new macro (or maybe you want to replace default macro with your like regex pattern) with `macrosurl.register(macro, pattern)` method.
25+
26+
Example of registration.
27+
28+
```python
29+
import macrosurl
30+
31+
macrosurl.register('myhash', '[a-f0-9]{9}')
32+
33+
urlpatterns = patterns(
34+
'yourapp.views',
35+
url('^:myhash/$', 'myhash_main'),
36+
url('^news/:news_myhash/$', 'myhash_news'),
37+
)
38+
```
39+
40+
You free to register custom macro anywhere (i do it in main urls.py file). Macros Urls uses lazy initiazation. Macros will be compiled only on first request.
41+
42+
### Urls normalization
43+
44+
Once Macros Url finished compile regex pattern, it makes normalization of it by rules:
45+
46+
- Strip from left side all whitespace and ^
47+
- Strip from right side of pattern all whitespace and $
48+
- Add to left side ^
49+
- Add to right side $
50+
51+
This makes your urls always very strong to adding any unexpected params into path.
52+
53+
### Examples
54+
55+
Macro Url example urls.py file
56+
57+
```python
58+
from django.conf.urls import patterns
59+
from project.apps.portal import views
60+
from project.apps.macrosurl import url
61+
62+
63+
urlpatterns = patterns(
64+
'yourapp.views',
65+
url('^:category_slug/$', 'category'),
66+
url('^:category_slug/:product_slug/$', 'category_product'),
67+
url('^:category_slug/:product_slug/:variant_id$', 'category_product_variant'),
68+
url('^news/$', 'news'),
69+
url('^news/:year/:month/:day$', 'news_date'),
70+
url('^news/:slug$', 'news_entry'),
71+
url('^order/:id$', 'order'),
72+
)
73+
```
74+
75+
Django way urls example
76+
77+
```python
78+
from django.conf.urls import patterns
79+
from project.apps.portal import views
80+
from project.apps.macrosurl import url
81+
82+
83+
urlpatterns = patterns(
84+
'yourapp.views',
85+
url('^(?P<category_slug>[\w-]+>)/$', 'category'),
86+
url('^(?P<category_slug>[\w-]+>)/(?P<product_slug>[\w-]+>)/$', 'category_product'),
87+
url('^(?P<category_slug>[\w-]+>)/(?P<product_slug>[\w-]+>)/(?P<variant_id>\d+>)$', 'category_product_variant'),
88+
url('^news/$', 'news'),
89+
url('^news/(?P<year>\d{4}>)/(?P<month>(0?([1-9])|10|11|12)>)/(?P<day>((0|1|2)?([1-9])|[1-3]0|31)>)$', 'news_date'),
90+
url('^news/(?P<slug>[\w-]+>)$', 'news_entry'),
91+
url('^order/(?P<id>\d+>)$', 'order'),
92+
)
93+
```
94+
95+
I think you simple understand the difference of ways :)
96+
97+
#### Routing must be simple! ;-)
98+
99+
I think real raw url regexp patterns need in 1% case only. Prefer simple way to write (and read, this is important) fancy clean urls.
100+
101+
### Contributor
102+
103+
Alexandr Shurigin (aka [phpdude](https://github.com/phpdude/))

macrosurl/__init__.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import re
2+
3+
from django.conf.urls import url as baseurl
4+
5+
VERSION = (0, 1, 0)
6+
7+
_macros_library = {
8+
'id': r'\d+',
9+
'slug': r'[\w-]+',
10+
'year': r'\d{4}',
11+
'month': r'(0?([1-9])|10|11|12)',
12+
'day': r'((0|1|2)?([1-9])|[1-3]0|31)',
13+
'date': r'\d{4}-(0?([1-9])|10|11|12)-((0|1|2)?([1-9])|[1-3]0|31)',
14+
'uuid': r'[a-fA-F0-9]{8}-?[a-fA-F0-9]{4}-?[a-fA-F0-9]{4}-?[a-fA-F0-9]{4}-?[a-fA-F0-9]{12}'
15+
}
16+
17+
18+
def get_version(*args, **kwargs):
19+
return ".".join(map(str, VERSION))
20+
21+
22+
def register(macros, pattern):
23+
_macros_library[macros] = pattern
24+
25+
26+
def regex_group(macro, pattern):
27+
return '(?P<%s>%s)' % (macro, pattern)
28+
29+
30+
def normalize_pattern(url):
31+
return '^%s$' % url.lstrip("^ \n").rstrip("$ \n")
32+
33+
34+
class MacroUrlPattern(object):
35+
def __init__(self, pattern):
36+
self.pattern = pattern
37+
38+
39+
def compile(self):
40+
pattern = self.pattern
41+
macros = re.findall('(:([a-z_\d]+))', pattern)
42+
for match, macro in macros:
43+
if macro in _macros_library:
44+
pattern = pattern.replace(match, regex_group(macro, _macros_library[macro]))
45+
else:
46+
for _macro in _macros_library:
47+
if macro.endswith("_%s" % _macro):
48+
pattern = pattern.replace(match, regex_group(macro, _macros_library[_macro]))
49+
continue
50+
51+
return normalize_pattern(pattern)
52+
53+
@property
54+
def compiled(self):
55+
if not hasattr(self, '_compiled'):
56+
setattr(self, '_compiled', self.compile())
57+
58+
return getattr(self, '_compiled')
59+
60+
def __str__(self):
61+
return self.compiled
62+
63+
def __unicode__(self):
64+
return self.__str__()
65+
66+
67+
def url(regex, view, kwargs=None, name=None, prefix=''):
68+
return baseurl(MacroUrlPattern(regex), view, kwargs=kwargs, name=name, prefix=prefix)

setup.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#!/usr/bin/env python
2+
import os
3+
4+
from setuptools import setup, find_packages
5+
6+
7+
def read(filename):
8+
return open(os.path.join(os.path.dirname(__file__), filename)).read()
9+
10+
11+
setup(
12+
name='django-macros-url',
13+
version=__import__('macrosurl').get_version(),
14+
author='Alexandr Shurigin',
15+
author_email='alexandr.shurigin@gmail.com',
16+
description='Macros Url library for django',
17+
license='BSD',
18+
keywords='django macros url pattern regex',
19+
url='https://github.com/phpdude/django-macros-url',
20+
packages=find_packages(exclude=['tests']),
21+
include_package_data=True,
22+
test_suite='tests',
23+
long_description=read("README.markdown"),
24+
classifiers=[
25+
'Development Status :: 4 - Beta',
26+
'Environment :: Console',
27+
'Environment :: Plugins',
28+
'Framework :: Django',
29+
'Intended Audience :: Developers',
30+
'Intended Audience :: System Administrators',
31+
'Intended Audience :: Information Technology',
32+
'License :: OSI Approved :: MIT License',
33+
'Natural Language :: English',
34+
'Operating System :: OS Independent',
35+
'Programming Language :: Python',
36+
'Topic :: Internet :: WWW/HTTP',
37+
'Topic :: Internet :: WWW/HTTP :: WSGI',
38+
'Topic :: Software Development :: Libraries',
39+
'Topic :: Software Development :: Libraries :: Python Modules',
40+
'Topic :: Software Development :: Pre-processors'
41+
],
42+
requires=['django']
43+
)

tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__author__ = 'dude'

tests/urls.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import unittest
2+
import uuid
3+
4+
from django.conf import settings
5+
6+
from macrosurl import MacroUrlPattern, url
7+
8+
9+
class TestRegexCompilation(unittest.TestCase):
10+
def test_nomacro(self):
11+
self.assertEqual(MacroUrlPattern('^$').compiled, '^$')
12+
self.assertEqual(MacroUrlPattern('^news/all/$').compiled, '^news/all/$')
13+
self.assertEqual(MacroUrlPattern('^news/all/$').compiled, '^news/all/$')
14+
self.assertEqual(MacroUrlPattern('^news/all/$').compiled, '^news/all/$')
15+
self.assertEqual(MacroUrlPattern('^news/:news/$').compiled, '^news/:news/$')
16+
17+
def test_normalize_url(self):
18+
self.assertEqual(MacroUrlPattern('').compiled, '^$')
19+
self.assertEqual(MacroUrlPattern('news/all/').compiled, '^news/all/$')
20+
self.assertEqual(MacroUrlPattern('^news/all/$').compiled, '^news/all/$')
21+
self.assertEqual(MacroUrlPattern('^news/all/').compiled, '^news/all/$')
22+
23+
def test_strip_whitespace(self):
24+
self.assertEqual(MacroUrlPattern('').compiled, '^$')
25+
self.assertEqual(MacroUrlPattern(' news/all/').compiled, '^news/all/$')
26+
self.assertEqual(MacroUrlPattern('^news/all/$ ').compiled, '^news/all/$')
27+
self.assertEqual(MacroUrlPattern(' ^news/all/ ').compiled, '^news/all/$')
28+
self.assertEqual(MacroUrlPattern(' ^news/all/ \n').compiled, '^news/all/$')
29+
30+
def test_id(self):
31+
self.assertEqual(MacroUrlPattern('page/:id').compiled, '^page/(?P<id>\d+)$')
32+
self.assertEqual(MacroUrlPattern('product/:product_id').compiled, '^product/(?P<product_id>\d+)$')
33+
self.assertEqual(MacroUrlPattern('product/:id/:product_id').compiled,
34+
'^product/(?P<id>\d+)/(?P<product_id>\d+)$')
35+
self.assertEqual(MacroUrlPattern('product/:id/:product_id/:news_id').compiled,
36+
'^product/(?P<id>\d+)/(?P<product_id>\d+)/(?P<news_id>\d+)$')
37+
38+
def test_slug(self):
39+
self.assertEqual(MacroUrlPattern('page/:slug').compiled, '^page/(?P<slug>[\w-]+)$')
40+
self.assertEqual(MacroUrlPattern('page/:category_slug/:slug').compiled,
41+
'^page/(?P<category_slug>[\w-]+)/(?P<slug>[\w-]+)$')
42+
43+
def test_year(self):
44+
self.assertEqual(MacroUrlPattern('news/:year').compiled, '^news/(?P<year>\d{4})$')
45+
46+
def test_year_month(self):
47+
self.assertEqual(MacroUrlPattern('news/:year/:month').compiled,
48+
'^news/(?P<year>\d{4})/(?P<month>(0?([1-9])|10|11|12))$')
49+
50+
def test_year_month_day(self):
51+
self.assertEqual(MacroUrlPattern('news/:year/:month/:day/').compiled,
52+
'^news/(?P<year>\d{4})/(?P<month>(0?([1-9])|10|11|12))/(?P<day>((0|1|2)?([1-9])|[1-3]0|31))/$')
53+
54+
def test_date(self):
55+
self.assertEqual(MacroUrlPattern('news/:date/').compiled,
56+
'^news/(?P<date>\d{4}-(0?([1-9])|10|11|12)-((0|1|2)?([1-9])|[1-3]0|31))/$')
57+
58+
def test_uid(self):
59+
self.assertEqual(MacroUrlPattern('invoice/:uuid').compiled,
60+
'^invoice/(?P<uuid>[a-fA-F0-9]{8}-?[a-fA-F0-9]{4}-?[a-fA-F0-9]{4}-?[a-fA-F0-9]{4}-?[a-fA-F0-9]{12})$')
61+
62+
def test_strongurl(self):
63+
self.assertEqual(MacroUrlPattern('orders/:date/:uuid/products/:slug/:variant_id').compiled,
64+
'^orders/(?P<date>\\d{4}-(0?([1-9])|10|11|12)-((0|1|2)?([1-9])|[1-3]0|31))/(?P<uuid>[a-fA-F0-9]{8}-?[a-fA-F0-9]{4}-?[a-fA-F0-9]{4}-?[a-fA-F0-9]{4}-?[a-fA-F0-9]{12})/products/(?P<slug>[\\w-]+)/(?P<variant_id>\\d+)$')
65+
66+
67+
class TestRegexUrlResolving(unittest.TestCase):
68+
def setUp(self):
69+
if not settings.configured:
70+
settings.configure(USE_I18N=False)
71+
72+
def test_id(self):
73+
self.assertIsNone(url('product/:id', 'view').resolve('product/test'))
74+
self.assertIsNotNone(url('product/:id', 'view').resolve('product/10'))
75+
self.assertEqual(url('product/:id', 'view').resolve('product/10').kwargs['id'], '10')
76+
self.assertEqual(url('product/:product_id', 'view').resolve('product/10').kwargs['product_id'], '10')
77+
78+
def test_slug(self):
79+
self.assertIsNone(url('product/:slug', 'view').resolve('product/test/ouch'))
80+
self.assertIsNotNone(url('product/:slug', 'view').resolve('product/test'))
81+
self.assertIsNotNone(url('product/:slug/:other_slug', 'view').resolve('product/test/other'))
82+
83+
def test_year(self):
84+
self.assertIsNone(url('news/:year', 'view').resolve('news/last'))
85+
for y in range(1970, 2025):
86+
self.assertIsNotNone(url('news/:year', 'view').resolve('news/%s' % y))
87+
self.assertIsNone(url('news/:year/last', 'view').resolve('news/2014/other'))
88+
self.assertIsNotNone(url('news/:year/last', 'view').resolve('news/2014/last'))
89+
90+
def test_year_month(self):
91+
self.assertIsNone(url('news/:year/:month', 'view').resolve('news/2014/last'))
92+
self.assertIsNone(url('news/:year/:month', 'view').resolve('news/2014/2012'))
93+
for y in range(1970, 2025):
94+
for m in range(1, 12):
95+
self.assertIsNotNone(url('news/:year/:month', 'view').resolve('news/%s/%s' % (y, m)))
96+
97+
self.assertIsNotNone(url('news/:year/:month/last', 'view').resolve('news/2014/12/last'))
98+
99+
def test_year_month_day(self):
100+
self.assertIsNone(url('news/:year/:month/:day', 'view').resolve('news/2014/12/last'))
101+
self.assertIsNone(url('news/:year/:month/:day', 'view').resolve('news/2014/2012/31'))
102+
for y in range(2000, 2020):
103+
for m in range(1, 12):
104+
for d in range(1, 31):
105+
self.assertIsNotNone(url('news/:year/:month/:day', 'view').resolve('news/%s/%s/%s' % (y, m, d)))
106+
107+
def test_date(self):
108+
self.assertIsNone(url('news/:date', 'view').resolve('news/2014/12/12'))
109+
for y in range(2000, 2020):
110+
for m in range(1, 12):
111+
for d in range(1, 31):
112+
self.assertIsNotNone(url('news/:date', 'view').resolve('news/%s-%s-%s' % (y, m, d)))
113+
114+
def test_uuid(self):
115+
self.assertIsNone(url("invoice/:uuid", 'view').resolve('invoice/123123-123123-1231231-1231312-3-1312312-'))
116+
for i in range(1, 1000):
117+
self.assertIsNotNone(url("invoice/:uuid", 'view').resolve('invoice/%s' % uuid.uuid4()))

0 commit comments

Comments
 (0)