├── tests ├── __init__.py └── urls.py ├── MANIFEST.in ├── setup.cfg ├── djurlheader.png ├── CONTRIBUTORS.md ├── .travis.yml ├── LICENSE.txt ├── FAQ.md ├── CHANGELOG.md ├── .gitignore ├── setup.py ├── __init__.py ├── CODE_OF_CONDUCT.md ├── djurl └── __init__.py └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file= README.md -------------------------------------------------------------------------------- /djurlheader.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/venturachrisdev/djurl/HEAD/djurlheader.png -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | Contributors 2 | === 3 | * [Christopher Ventura](https://github.com/venturachrisdev) <> 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: false 3 | python: 4 | - "2.7" 5 | - "3.4" 6 | - "3.6" 7 | 8 | env: 9 | - DJANGO=1.10 10 | 11 | install: 12 | - "pip install -q django==$DJANGO" 13 | 14 | script: 15 | - "python setup.py test" -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2017 Christopher Ventura 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /FAQ.md: -------------------------------------------------------------------------------- 1 | FAQ 2 | ==== 3 | 4 | * **How do I replace django urls with DjUrl in my current project?** 5 | 6 | Easy, just change the Django default `url()` method: 7 | ```python 8 | from django.conf.urls import url 9 | ``` 10 | with DjUrl's `url()`: 11 | ```python 12 | from djurl import url 13 | ``` 14 | Then, be sure to replace all the regular expression with Djurl's syntax and if you're using `Class Based Views`, feel free to remove the `as_view()` call. 15 | 16 | * **If there's a new version of Djurl, how do I reinstall it?** 17 | 18 | You can reinstall Djurl via pip typing: 19 | ``` 20 | $ python -m pip install djurl -U --force-reinstall --no-cache-dir 21 | ``` 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | v0.2.0 2 | --- 3 | * removed unnecesary `print()` statement. 4 | * Added *pep8* recommendations. 5 | * Added `:filename` pattern. 6 | * Added `:uuid` pattern. 7 | * Added `:query` pattern. 8 | * Unregisted patterns are of type `:slug` by default. 9 | 10 | v0.1.3 11 | --- 12 | * Fixed fatal bug when using nested routes. 13 | 14 | v0.1.2 15 | --- 16 | * Fixed bug when `route == '^$'` or `route == '/'` 17 | * Removed adding `/` at the end of the route if it's not an exact route. 18 | * Trim route with spaces. Ex: `' news/today '` => `'^news/today$'` 19 | * Fixed bug when a route ended with a slash and contained spaces. Ej: `' /users/ '` 20 | 21 | v0.1.1 22 | --- 23 | * Changed package description 24 | 25 | v0.1.0 26 | --- 27 | * Basic functionality -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # Installer logs 28 | pip-log.txt 29 | pip-delete-this-directory.txt 30 | 31 | # Unit test / coverage reports 32 | htmlcov/ 33 | .tox/ 34 | .coverage 35 | .coverage.* 36 | .cache 37 | nosetests.xml 38 | coverage.xml 39 | *,cover 40 | .hypothesis/ 41 | 42 | # Translations 43 | *.pot 44 | 45 | *.log 46 | 47 | # Sphinx documentation 48 | docs/_build/ 49 | 50 | # PyBuilder 51 | target/ 52 | 53 | # PyCharm 54 | .idea/ 55 | .env 56 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | 4 | def read(filename): 5 | import os 6 | return open(os.path.join(os.path.dirname(__file__), filename)).read() 7 | 8 | 9 | setup( 10 | name="djurl", 11 | version=__import__('djurl').get_version(), 12 | author="Christopher Ventura", 13 | author_email="venturachrisdev@gmail.com", 14 | description="Simple yet helpful library for writing Django urls by an easy, short an intuitive way.", 15 | url="https://github.com/venturachrisdev/djurl", 16 | license="MIT", 17 | keywords="django url urlparse web python regex", 18 | packages=find_packages(exclude=['tests']), 19 | include_package_data=True, 20 | test_suite="tests", 21 | long_description=read('README.md'), 22 | install_requires=['django'], 23 | classifiers=[ 24 | 'Development Status :: 5 - Production/Stable', 25 | 'Environment :: Console', 26 | 'Environment :: Plugins', 27 | 'Framework :: Django', 28 | 'Intended Audience :: Developers', 29 | 'Intended Audience :: System Administrators', 30 | 'Intended Audience :: Information Technology', 31 | 'License :: OSI Approved :: MIT License', 32 | 'Natural Language :: English', 33 | 'Operating System :: OS Independent', 34 | 'Programming Language :: Python', 35 | 'Topic :: Internet :: WWW/HTTP', 36 | 'Topic :: Internet :: WWW/HTTP :: WSGI', 37 | 'Topic :: Software Development :: Libraries', 38 | 'Topic :: Software Development :: Libraries :: Python Modules', 39 | 'Topic :: Software Development :: Pre-processors' 40 | ] 41 | ) 42 | 43 | print(read('README.md')) 44 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | 2 | VERSION = (0,1,2) 3 | 4 | def get_version(): 5 | return ".".join(map(str, VERSION)) 6 | 7 | _default_patterns_ = { 8 | # Keys 9 | 'pk': r'\d+', 10 | 'id': r'\d+', 11 | 'slug': r'[A-Za-z0-9_-]+', 12 | # Date 13 | 'day': r'(([0-2])?([1-9])|[1-3]0|31)', 14 | 'month': r'(0?[1-9]|10|11|12)', 15 | 'year': r'\w{4}', 16 | 'date': r'\w{4}-(0?([1-9])|10|11|12)-((0|1|2)?([1-9])|[1-3]0|31)', 17 | # filters 18 | 'page': r'\d+', 19 | 'filename': r'[\w,\s-]+\.[A-Za-z]{2,4}', 20 | } 21 | 22 | """ 23 | Classname: Djurl 24 | Description: Django Url 25 | Author: Christopher Ventura Aguiar 26 | Date: Jun 22, 2017 27 | Long description: 28 | 29 | """ 30 | class Djurl(): 31 | def __init__(self, pattern, exact=True): 32 | self.pattern = pattern 33 | self.exact = exact 34 | if self.pattern.startswith('/'): 35 | self.pattern = self.pattern[1:] 36 | 37 | def normalize(self, path): 38 | stpath = path.lstrip("^\n") 39 | if len(stpath) > 1: 40 | stpath = stpath.lstrip("//"); 41 | stpath = stpath.rstrip(" $\n") 42 | result = "^%s" 43 | if self.exact: 44 | result += "$" 45 | 46 | # Trim 47 | return result % stpath 48 | 49 | def trim(self, path): 50 | return path.replace(" ", "") 51 | 52 | def create_pattern(self, key, pattern): 53 | return "(?P<%s>%s)" % (key, pattern) 54 | 55 | #core 56 | def build(self): 57 | 58 | built = self.trim(self.pattern) 59 | if len(built) > 1: 60 | import re 61 | paramkeys = re.findall('(:([a-z_\d]+))', built) 62 | for matches, key in paramkeys: 63 | if key in _default_patterns_: 64 | newpattern = self.create_pattern(key, _default_patterns_[key]) 65 | built = built.replace(":%s" % key, newpattern) 66 | print(built) 67 | else: 68 | for x in _default_patterns_: 69 | if key.endswith('_%s' % x): 70 | newpattern = self.create_pattern(key, _default_patterns_[x]) 71 | built = built.replace(":%s" % key, newpattern) 72 | if not built.endswith('/') and not built.endswith('$'): 73 | built += '/' 74 | 75 | result = self.normalize(built) 76 | return result 77 | 78 | @property 79 | def built(self): 80 | if not hasattr(self, '_built'): 81 | setattr(self, '_built', self.build()) 82 | 83 | return getattr(self, '_built') 84 | 85 | def __str__(self): 86 | return self.built 87 | 88 | #compatibily with python 2 89 | def __unicode__(self): 90 | return self.__str__() 91 | 92 | 93 | def register_pattern(key, pattern): 94 | _default_patterns_[key] = pattern 95 | 96 | def url(pattern, view, kwargs=None, name=None): 97 | exact = True 98 | v = view 99 | 100 | # Class Based Views 101 | if isinstance(view, type): 102 | if hasattr(view, 'as_view'): 103 | v = view.as_view() 104 | 105 | # include 106 | if isinstance(v, tuple): 107 | exact = False 108 | 109 | from django.conf.urls import url as BaseUrl 110 | return BaseUrl(Djurl(pattern, exact=exact), v, kwargs=kwargs, name=name) -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at venturachrisdev@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /djurl/__init__.py: -------------------------------------------------------------------------------- 1 | VERSION = (0, 2, 0) 2 | 3 | 4 | def get_version(): 5 | return ".".join(map(str, VERSION)) 6 | 7 | """ 8 | TODO: Find out another way to regex 'query' without all that %#$$*# 9 | """ 10 | 11 | _default_patterns_ = { 12 | # Keys 13 | 'pk': r'\d+', 14 | 'id': r'\d+', 15 | 'slug': r'[A-Za-z0-9_-]+', 16 | # Date 17 | 'day': r'(([0-2])?([1-9])|[1-3]0|31)', 18 | 'month': r'(0?[1-9]|10|11|12)', 19 | 'year': r'\w{4}', 20 | 'date': r'\w{4}-(0?([1-9])|10|11|12)-((0|1|2)?([1-9])|[1-3]0|31)', 21 | # filters 22 | 'page': r'\d+', 23 | 'filename': r'[\w,\s-]+\.[A-Za-z]{2,4}', 24 | '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}){1}', 25 | 'query': r'(\w|\.|\,|\/|\\|\=|[%!$`&*()+@_-])+', 26 | } 27 | 28 | 29 | class Djurl(): 30 | """ 31 | Classname: Djurl 32 | Description: Django Url 33 | Author: Christopher Ventura Aguiar 34 | Date: Jun 22, 2017 35 | Long description: 36 | """ 37 | def __init__(self, pattern, exact=True, father=False): 38 | self.pattern = pattern 39 | self.father = father 40 | self.exact = exact 41 | if self.pattern.startswith('/'): 42 | self.pattern = self.pattern[1:] 43 | 44 | def normalize(self, path): 45 | stpath = path.lstrip("^\n") 46 | if len(stpath) > 1: 47 | stpath = stpath.lstrip("//") 48 | stpath = stpath.rstrip(" $\n") 49 | result = "^%s" 50 | if self.exact: 51 | result += "$" 52 | 53 | # Trim 54 | return result % stpath 55 | 56 | def trim(self, path): 57 | return path.replace(" ", "") 58 | 59 | def create_pattern(self, key, pattern): 60 | return "(?P<%s>%s)" % (key, pattern) 61 | 62 | # core 63 | def build(self): 64 | built = self.trim(self.pattern) 65 | if len(built) > 1: 66 | import re 67 | paramkeys = re.findall('(:([a-z_\d]+))', built) 68 | for matches, key in paramkeys: 69 | if key in _default_patterns_: 70 | newpattern = self.create_pattern(key, _default_patterns_[key]) 71 | built = built.replace(":%s" % key, newpattern) 72 | done = True 73 | else: 74 | done = False 75 | for x in _default_patterns_: 76 | if key.endswith('_%s' % x): 77 | newpattern = self.create_pattern(key, _default_patterns_[x]) 78 | built = built.replace(":%s" % key, newpattern) 79 | done = True 80 | # Slug by default 81 | if not done: 82 | newpattern = self.create_pattern(key, _default_patterns_['slug']) 83 | built = built.replace(":%s" % key, newpattern) 84 | 85 | 86 | if not built.endswith('/') and not built.endswith('$'): 87 | if self.exact or (not self.exact and self.father): 88 | built += '/' 89 | 90 | result = self.normalize(built) 91 | return result 92 | 93 | @property 94 | def built(self): 95 | if not hasattr(self, '_built'): 96 | setattr(self, '_built', self.build()) 97 | 98 | return getattr(self, '_built') 99 | 100 | def __str__(self): 101 | return self.built 102 | 103 | # compatibily with python 2 104 | def __unicode__(self): 105 | return self.__str__() 106 | 107 | 108 | def register_pattern(key, pattern): 109 | _default_patterns_[key] = pattern 110 | 111 | 112 | def url(pattern, view, kwargs=None, name=None): 113 | exact = True 114 | father = False 115 | v = view 116 | 117 | # Class Based Views 118 | if isinstance(view, type): 119 | if hasattr(view, 'as_view'): 120 | v = view.as_view() 121 | 122 | # include 123 | if isinstance(v, tuple): 124 | exact = False 125 | father = True 126 | 127 | from django.conf.urls import url as BaseUrl 128 | return BaseUrl(Djurl(pattern, exact=exact, father=father), v, kwargs=kwargs, name=name) 129 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![DjUrl - Django urls](/djurlheader.png) [![Build Status](https://travis-ci.org/venturachrisdev/djurl.svg?branch=master)](https://travis-ci.org/venturachrisdev/djurl) 2 | === 3 | Simple yet helpful library for writing Django urls by an easy, short and intuitive way. 4 | 5 | Why should I use DjUrl? 6 | --- 7 | Django routing urls aren't easy to deal with, regular expressions can become a nightmare sometimes. Just imagine dealing with such routes in your `django app`: 8 | ```python 9 | from django.conf.urls import url 10 | from core import BlogView, SinglePostView, SearchResultsView, ArchiveView 11 | 12 | urlpatterns = [ 13 | # => /blog/ 14 | url(r'^blog/$', BlogView.as_view(), name="blog"), 15 | # => /blog/5 16 | url(r'^blog/(?P[0-9]+)/$', SinglePostView.as_view(), name="singlepost"), 17 | # => /blog/search/sometitle 18 | url(r'^blog/search/(?P[A-Za-z0-9_-]+)/$', SearchResultsView.as_view(), name="search"), 19 | # => /blog/archive/2017/02/12 20 | url(r'^blog/archive/(?P[0-9]{4}-(0?([1-9])|10|11|12)-((0|1|2)?([1-9])|[1-3]0|31))/$', 21 | ArchiveView.as_view(), name="archive") 22 | ] 23 | ``` 24 | That's too much work and you lost me in those regex. With **DjUrl** this comes easy, you just need to *express what you want*, **DjUrl will handle the regular expressions for you**: 25 | 26 | ```python 27 | from djurl import url 28 | from core import BlogView, SinglePostView, SearchResultsView, ArchiveView 29 | 30 | urlpatterns = [ 31 | url('/blog', BlogView, name="blog"), 32 | url('/blog/:id', SinglePostView, name="singlepost"), 33 | url('/blog/search/:query', SearchResultsView, name="search"), 34 | url('/blog/archive/:date', ArchiveView, name="archive") 35 | ] 36 | ``` 37 | No regex, just clean paths and param names. You can now pass the regex work to DjUrl and concentrate in the *bussiness logic*. It saves you a lot of time and code. *You don't need to worry about the routes anymore*. **Note you don't need to call `as_view` in your CBV's.** DjUrl does this for you as well. 38 | 39 | Usage 40 | --- 41 | Now you know what you should use `DjUrl`, It's time to learn how to use it. DjUrl has a list of known/default pattern that you can use in your routes, these are: 42 | 43 | * `id`: A secuence of characters from 0 to 9. Ej: `1, 12, 454545, 8885500, 8` 44 | * `pk`: A primary key, it's like `id` but needed for `Class Based Views`. 45 | * `page`: falls in the same category, but you'd use `page` for a better param name. 46 | * `slug`: A simple string (alphanumeric characters). 47 | * `query`: A search parameter. It allows some special characters that *slug* doesn't. Ex: `hello%20word`, `don%27t_quote-me` 48 | * `day`: A number between 01,..., 31. 49 | * `month`: A number between 01,...,12. 50 | * `year`: A four digits number: `1998, 2017, 2018, 3015, 2020, 1406...` 51 | * `date`: An expression with `year-month-day` format: `2017-06-23, 1998-10-20, 1492-10-12` 52 | * `filename`: An expression with `*.\w{2,4}` format: `index.js`, `detail.html`, `'my_book.pdf'`, `'dfj358h-g9854-fn84n4.tmp'` 53 | * `UUID`: *Universally unique identifier* is a 128-bit number used to identify information in computer systems. Use a format as `xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx`. Ex: `123e4567-e89b-12d3-a456-426655440000` 54 | 55 | That means, wherever you put `/:id` you can use it in your view as param (named `id`). 56 | ```python 57 | url('post/:pk/comment/:id', myview, name="post_comment") 58 | ``` 59 | Your view: 60 | ```python 61 | def myview(request, pk, id): 62 | # Use `pk` (post's) and `id` (comment's) 63 | ``` 64 | 65 | But what if I have two or more id's, or two slugs? What if I wanted to use a custom name for my id's? - Ok, you can use custom names if you end it with `_` + the pattern type. - What?... 66 | ```python 67 | url('post/:post_pk/comment/:comment_id', myview, ...) 68 | # ... 69 | def myview(request, post_pk, comment_id): 70 | # `post_pk` is parsed as a :pk and `comment_id` like an :id 71 | 72 | ``` 73 | Yeah, it sounds good!, but... What if I wanted to use my own patterns? - Easy, any world in the path is of type `:slug` by default, but if you need a custom pattern you can register many as you want: 74 | ```python 75 | from djurl import url, register_pattern 76 | register_pattern('hash', '[a-f0-9]{9}') 77 | # parsed as slug 78 | url('/:user', myUserView), 79 | # custom pattern 80 | url('/:hash', myview), 81 | ``` 82 | 83 | If you have questions, visit our [FAQ's](FAQ.md) or open an *issue*. 84 | 85 | Install 86 | --- 87 | If you want to have fun with this library and integrate it to your project, just type in your terminal: 88 | ``` 89 | $ pip install djurl 90 | ``` 91 | or, clone the repo and type: 92 | ``` 93 | $ python setup.py install 94 | ``` 95 | Enjoy it! 96 | 97 | Testing 98 | --- 99 | Clone the repo and run Djurl tests by: 100 | ``` 101 | $ python setup.py test 102 | ``` 103 | 104 | Contributions 105 | --- 106 | If you've found a bug/error or just have questions, feel free to open an **issue**. And, **Pull requests** are welcome as well. 107 | Don't forget to add your name to [CONTRIBUTORS.md](CONTRIBUTORS.md) 108 | 109 | License 110 | ======= 111 | 112 | Copyright 2017 Christopher Ventura 113 | 114 | Licensed under the Apache License, Version 2.0 (the "License"); 115 | you may not use this file except in compliance with the License. 116 | You may obtain a copy of the License at 117 | 118 | http://www.apache.org/licenses/LICENSE-2.0 119 | 120 | Unless required by applicable law or agreed to in writing, software 121 | distributed under the License is distributed on an "AS IS" BASIS, 122 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 123 | See the License for the specific language governing permissions and 124 | limitations under the License. 125 | -------------------------------------------------------------------------------- /tests/urls.py: -------------------------------------------------------------------------------- 1 | # Test 2 | import sys 3 | 4 | from djurl import Djurl 5 | 6 | 7 | if sys.version_info >= (2, 7): 8 | import unittest 9 | else: 10 | from django.utils import unittest 11 | 12 | if sys.version_info >= (3, 4): 13 | from re import fullmatch 14 | else: 15 | import re 16 | """ 17 | Emulate python-3.4 re.fullmatch(). 18 | 19 | https://stackoverflow.com/questions/30212413/backport-python-3-4s-regular-expression-fullmatch-to-python-2 20 | """ 21 | def fullmatch(pattern, string): 22 | return re.match("(?:%s)\Z" % pattern, string) 23 | 24 | 25 | def build(pattern, exact=True): 26 | return Djurl(pattern, exact=exact).build() 27 | 28 | def evaluate(pattern, path, exact=True): 29 | # Return True if the path matches with the given pattern, False otherwise 30 | p = build(pattern, exact=exact) 31 | if exact and len(path) > 1: 32 | path = path + '/' 33 | 34 | return fullmatch(p, path) 35 | 36 | 37 | class TestRegexBuilding(unittest.TestCase): 38 | 39 | def test_building_without_pattern(self): 40 | """ 41 | If it's not broken, don't fix it. 42 | Testing: If I don't provide a pattern to replace in my route, Djurl doesn't need 43 | to touch it. Just adding '^' and|or '$' if they weren't provided. Also, if you provide a pattern not registed in djurl default pattern, the library should keep it there and not parse it. 44 | """ 45 | self.assertEqual(build('^$'), '^$') 46 | self.assertEqual(build('^blog/$'), '^blog/$') 47 | self.assertEqual(build('^me/15/$'), '^me/15/$') 48 | self.assertEqual(build('^father/', exact=False), '^father/') 49 | self.assertEqual(build('/', exact=False), '^') 50 | self.assertEqual(build('^*.jpg|png|gif|jpeg$'), '^*.jpg|png|gif|jpeg$') 51 | self.assertEqual(build('^/', exact=False), '^/') 52 | 53 | def test_strip_slashes_at_beginning(self): 54 | """ 55 | When you're working with a Django url, you don't use '/' at the beginning of the 56 | route, you start with '^' instead. If we are not working with regex, it's common 57 | to put '/' at the beginning of a route. Djurl should strip that slashes so the translation to django urlpatterns becomes easy. 58 | """ 59 | self.assertEqual(build('/about'), '^about/$') 60 | self.assertEqual(build('/hello/world'), '^hello/world/$') 61 | self.assertEqual(build('/me', exact=False), '^me') 62 | 63 | def test_add_slash_at_end_for_exact_routes(self): 64 | """ 65 | The exact url only matches with one path. Example: '^hello$' only matches with 'hello', not with 'hhello', 'helloo' nor, '.hello'. So we could say, it doesn't matter if an exact route ends with '/' because only will matches with one path ('^hello$' or '^hello/$' don't match with '/hello/3'). 66 | But if our route is not exact. Then should be able to match with more than one path. Example: '^hello' matches with 'hellooooo', hello/4', 'hello/hello/h' and so on. 67 | Then, we should only add a '/' if it's an exact pattern. 68 | """ 69 | self.assertEqual(build('/about'), '^about/$') 70 | self.assertEqual(build('/about/'), '^about/$') 71 | self.assertEqual(build('/about', exact=False), '^about') 72 | self.assertEqual(build('/about/', exact=False), '^about/') 73 | 74 | def test_normalize_url(self): 75 | self.assertEqual(build(''), '^$') 76 | self.assertEqual(build('/'), '^$') 77 | self.assertEqual(build('hello/world'), '^hello/world/$') 78 | self.assertEqual(build('/hello/world'), '^hello/world/$') 79 | self.assertEqual(build('/hello/world', exact=False), '^hello/world') 80 | 81 | self.assertEqual(build('/articles'), build('articles')) 82 | self.assertEqual(build('home'), build('/home/')) 83 | self.assertEqual(build('home/user/documents'), build('/home/user/documents/')) 84 | self.assertEqual(build(' news/today '), build('news/today')) 85 | 86 | def test_pattern_pk(self): 87 | self.assertEqual(build('/:pk'), '^(?P\d+)/$') 88 | self.assertEqual(build('/articles/:pk'), '^articles/(?P\d+)/$') 89 | self.assertEqual(build('/articles/:pk/comments'), '^articles/(?P\d+)/comments/$') 90 | 91 | def test_custom_patten_pk(self): 92 | self.assertEqual(build('/:user_pk'), '^(?P\d+)/$') 93 | self.assertEqual(build('/articles/:article_pk'), '^articles/(?P\d+)/$') 94 | self.assertEqual(build('/articles/:article_pk/comments'), '^articles/(?P\d+)/comments/$') 95 | 96 | def test_pattern_id(self): 97 | self.assertEqual(build('/:id'), '^(?P\d+)/$') 98 | self.assertEqual(build('/user/:id'), '^user/(?P\d+)/$') 99 | self.assertEqual(build('/user/:id/friends'), '^user/(?P\d+)/friends/$') 100 | 101 | def test_custom_pattern_id(self): 102 | self.assertEqual(build('/:user_id'), '^(?P\d+)/$') 103 | self.assertEqual(build('/user/:user_id'), '^user/(?P\d+)/$') 104 | self.assertEqual(build('/user/:user_id/friends'), '^user/(?P\d+)/friends/$') 105 | 106 | def test_pattern_slug(self): 107 | self.assertEqual(build('/:slug'), '^(?P[A-Za-z0-9_-]+)/$') 108 | self.assertEqual(build('/articles/:slug'), '^articles/(?P[A-Za-z0-9_-]+)/$') 109 | self.assertEqual(build('/post/:slug/comments'), '^post/(?P[A-Za-z0-9_-]+)/comments/$') 110 | 111 | # Custom 112 | self.assertEqual(build('/articles/:user'), '^articles/(?P[A-Za-z0-9_-]+)/$') 113 | 114 | def test_pattern_filename(self): 115 | self.assertEqual(build('/:filename'), '^(?P[\w,\s-]+\.[A-Za-z]{2,4})/$') 116 | self.assertEqual(build('/media/:filename'), '^media/(?P[\w,\s-]+\.[A-Za-z]{2,4})/$') 117 | 118 | def test_pattern_page(self): 119 | self.assertEqual(build('/:page'), '^(?P\d+)/$') 120 | self.assertEqual(build('/articles/:page'), '^articles/(?P\d+)/$') 121 | 122 | def test_custom_pattern_slug(self): 123 | self.assertEqual(build('/:post_slug'), '^(?P[A-Za-z0-9_-]+)/$') 124 | self.assertEqual(build('/articles/:article_slug'), '^articles/(?P[A-Za-z0-9_-]+)/$') 125 | self.assertEqual(build('/post/:post_slug/comments'), '^post/(?P[A-Za-z0-9_-]+)/comments/$') 126 | 127 | def test_custom_pattern_page(self): 128 | self.assertEqual(build('/:blog_page'), '^(?P\d+)/$') 129 | self.assertEqual(build('/articles/:article_page'), '^articles/(?P\d+)/$') 130 | 131 | def test_combined_patterns_in_same_route(self): 132 | self.assertEqual(build('/articles/:slug/comments/:id'), '^articles/(?P[A-Za-z0-9_-]+)/comments/(?P\d+)/$') 133 | self.assertEqual(build('/articles/:article_id/comments/:comment_id'), '^articles/(?P\d+)/comments/(?P\d+)/$') 134 | self.assertEqual(build('/user/:user_pk/status/:status_id'), '^user/(?P\d+)/status/(?P\d+)/$') 135 | self.assertEqual(build('/item/:pk/color/:slug'), '^item/(?P\d+)/color/(?P[A-Za-z0-9_-]+)/$') 136 | 137 | def test_pattern_day(self): 138 | self.assertEqual(build('/day/:day'), '^day/(?P(([0-2])?([1-9])|[1-3]0|31))/$') 139 | self.assertEqual(build('/report/:id/day/:day'), '^report/(?P\d+)/day/(?P(([0-2])?([1-9])|[1-3]0|31))/$') 140 | 141 | def test_pattern_month(self): 142 | self.assertEqual(build('/month/:month'), '^month/(?P(0?[1-9]|10|11|12))/$') 143 | self.assertEqual(build('/report/:id/month/:month/day/:day'), '^report/(?P\d+)/month/(?P(0?[1-9]|10|11|12))/day/(?P(([0-2])?([1-9])|[1-3]0|31))/$') 144 | 145 | def test_pattern_year(self): 146 | self.assertEqual(build('/archive/year/:year'), '^archive/year/(?P\w{4})/$') 147 | self.assertEqual(build('/report/:id/date/:year/:month/:day'), '^report/(?P\d+)/date/(?P\w{4})/(?P(0?[1-9]|10|11|12))/(?P(([0-2])?([1-9])|[1-3]0|31))/$') 148 | 149 | def test_pattern_date(self): 150 | self.assertEqual(build('/archive/date/:date'), '^archive/date/(?P\w{4}-(0?([1-9])|10|11|12)-((0|1|2)?([1-9])|[1-3]0|31))/$') 151 | 152 | """ 153 | Evaluation Tests 154 | """ 155 | 156 | def test_evaluate_basic(self): 157 | self.assertTrue(evaluate('/', '')) 158 | self.assertTrue(evaluate('/hello', 'hello')) 159 | self.assertTrue(evaluate('/home/', 'home')) 160 | self.assertTrue(evaluate('/article/:article', 'article/whatever')) 161 | self.assertTrue(evaluate(' /users/ ', 'users')) 162 | self.assertTrue(evaluate('/blog/:blog', 'blog/hello-world')) 163 | 164 | self.assertFalse(evaluate('/blog', 'blogggg')) 165 | self.assertFalse(evaluate('/article/:article', 'article/-5n877$@')) 166 | 167 | def test_evaluate_pk(self): 168 | self.assertTrue(evaluate('/:pk', '10')) 169 | self.assertTrue(evaluate('/post/:pk', 'post/4')) 170 | self.assertTrue(evaluate('/post/:pk', 'post/2345454')) 171 | 172 | self.assertFalse(evaluate('/post/:pk', 'post/hello-universe')) 173 | 174 | def test_evaluate_id(self): 175 | self.assertTrue(evaluate('/:id', '19090')) 176 | self.assertTrue(evaluate('/user/:id', 'user/3')) 177 | self.assertTrue(evaluate('/user/:id', 'user/53723')) 178 | 179 | self.assertFalse(evaluate('/user/:id', 'user/2017-10-20')) 180 | self.assertFalse(evaluate('/user/:id', 'post/96jhn869y6j')) 181 | 182 | def test_evaluate_slug(self): 183 | self.assertTrue(evaluate('/:slug', 'hello-world_3')) 184 | self.assertTrue(evaluate('/article/:slug', 'article/sherlock-holmes-season-2-episode-3-review')) 185 | self.assertTrue(evaluate('/article/:slug', 'article/58jtgj689hy')) 186 | 187 | self.assertFalse(evaluate('/item/:slug', 'item/2%20p40501rg%207#ktgi-10-20')) 188 | 189 | def test_evaluate_page(self): 190 | self.assertTrue(evaluate('/:page', '10')) 191 | self.assertTrue(evaluate('/magazine/page/:page', 'magazine/page/4')) 192 | self.assertTrue(evaluate('/magazine/page/:page', 'magazine/page/23')) 193 | 194 | self.assertFalse(evaluate('/magazine/:page', 'magazine/under-the-red_hood')) 195 | 196 | def test_evaluate_date(self): 197 | self.assertTrue(evaluate('/day/:day', 'day/02')) 198 | self.assertTrue(evaluate('/day/:day', 'day/2')) 199 | self.assertTrue(evaluate('/day/:day', 'day/31')) 200 | 201 | self.assertFalse(evaluate('/day/:day', 'day/32')) 202 | 203 | self.assertTrue(evaluate('/month/:month', 'month/01')) 204 | self.assertTrue(evaluate('/month/:month', 'month/2')) 205 | self.assertTrue(evaluate('/month/:month', 'month/12')) 206 | 207 | self.assertFalse(evaluate('/month/:month', 'month/13')) 208 | 209 | self.assertTrue(evaluate('/year/:year', 'year/1492')) 210 | self.assertTrue(evaluate('/year/:year', 'year/2016')) 211 | self.assertTrue(evaluate('/year/:year', 'year/4096')) 212 | 213 | self.assertFalse(evaluate('/year/:year', 'year/56000')) 214 | 215 | self.assertTrue(evaluate('/date/:day/:month/:year', 'date/31/10/2017')) 216 | self.assertTrue(evaluate('/date/:day/:month/:year', 'date/09/1/2017')) 217 | self.assertTrue(evaluate('/date/:day/:month/:year', 'date/3/01/2017')) 218 | self.assertTrue(evaluate('/date/:day/:month/:year', 'date/11/11/2011')) 219 | 220 | self.assertFalse(evaluate('/date/:day/:month/:year', 'date/32/10/2017')) 221 | self.assertFalse(evaluate('/date/:day/:month/:year', 'date/3/15/2017')) 222 | self.assertFalse(evaluate('/date/:day/:month/:year', 'date/11/11/20114')) 223 | 224 | self.assertTrue(evaluate('/date/:date', 'date/2017-10-31')) 225 | self.assertTrue(evaluate('/date/:date', 'date/2017-09-1')) 226 | self.assertTrue(evaluate('/date/:date', 'date/2017-01-3')) 227 | self.assertTrue(evaluate('/date/:date', 'date/2011-11-11')) 228 | 229 | self.assertFalse(evaluate('/date/:date', 'date/2011-11/11')) 230 | self.assertFalse(evaluate('/date/:date', 'date/2011_11_11')) 231 | self.assertFalse(evaluate('/date/:date', 'date/2011/11-11')) 232 | self.assertFalse(evaluate('/date/:date', 'date/2017-10-32')) 233 | self.assertFalse(evaluate('/date/:date', 'date/2017-15-3')) 234 | self.assertFalse(evaluate('/date/:date', 'date/20114-11-11')) 235 | 236 | def test_evaluate_filename(self): 237 | self.assertTrue(evaluate('/:filename', 'README.md')) 238 | self.assertTrue(evaluate('/:filename', 'license.txt')) 239 | self.assertTrue(evaluate('/:filename', 'my_first_book.pdf')) 240 | self.assertTrue(evaluate('/:filename', 'Main.java')) 241 | self.assertTrue(evaluate('/:filename', 'OldSchool.cpp')) 242 | self.assertTrue(evaluate('/:filename', '__init__.py')) 243 | self.assertTrue(evaluate('/:filename', 'style.css')) 244 | self.assertTrue(evaluate('/:filename', 'index.html')) 245 | self.assertTrue(evaluate('/:filename', 'h9g8984_3_g64_h00.tmp')) 246 | 247 | self.assertFalse(evaluate('/:filename', '.gitignore')) 248 | self.assertFalse(evaluate('/:filename', 'h9g8984_3_g64_h00')) 249 | self.assertFalse(evaluate('/:filename', 'home')) 250 | self.assertFalse(evaluate('/:filename', 'user/articles')) 251 | self.assertFalse(evaluate('/:filename', 'chapter/01')) 252 | 253 | def test_evaluate_uuid(self): 254 | self.assertTrue(evaluate('/:uuid', 'adcd3063-f29e-4d94-9d41-16dc80f33282')) 255 | self.assertTrue(evaluate('/uuid/:uuid', 'uuid/b15f994d-911a-4ee9-8658-2a69c6d9ef39')) 256 | self.assertTrue(evaluate('/user/:user_uuid/comments', 'user/499eb9c8-3642-47f3-8482-560c7a20c413/comments')) 257 | self.assertTrue(evaluate('/generate/:uuid', 'generate/fc3f6748-5e0e-11e7-907b-a6006ad3dba0')) 258 | self.assertTrue(evaluate('/u/:uuid', 'u/f72ec388-5e0f-11e7-907b-a6006ad3dba0')) 259 | 260 | self.assertFalse(evaluate('/uuid/:uuid', 'uuid/9j96ghn8g508604')) 261 | self.assertFalse(evaluate('/:uuid', '499b9c8-3642-47f-848-560c7c413')) 262 | 263 | def test_evaluate_query(self): 264 | self.assertTrue(evaluate('/:query', '00990998')) 265 | self.assertTrue(evaluate('/:query', 'aaaaaaaaa')) 266 | self.assertTrue(evaluate('/:query', '0df43tr-53535t')) 267 | self.assertTrue(evaluate('/:query', '859hg897895,/4r0r23449')) 268 | self.assertTrue(evaluate('/:query', 'hello%20work')) 269 | self.assertTrue(evaluate('/:query', 'hey_dude_whats_app')) 270 | self.assertTrue(evaluate('/:query', 'don%27t%20quote%20me&dkfjd2')) 271 | self.assertTrue(evaluate('/:query', 'version%20v.2.0')) 272 | self.assertTrue(evaluate('/:query', 'don%27t+let+go')) 273 | 274 | self.assertFalse(evaluate('/:query', 'don\'t quote me')) 275 | self.assertFalse(evaluate('/:query', 'Not gonna work')) 276 | self.assertFalse(evaluate('/:query', 'Maybe?')) --------------------------------------------------------------------------------