├── .github └── workflows │ └── python-package.yml ├── .gitignore ├── .hgignore ├── .hgtags ├── CHANGELOG.rst ├── INSTALL ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── TODO ├── doc-requirements.txt ├── docs ├── Makefile ├── changes.rst ├── conf.py ├── generating.rst ├── glossary.rst ├── index.rst ├── introduction.rst ├── modules │ ├── index.rst │ ├── mapper.rst │ ├── middleware.rst │ ├── route.rst │ ├── routes.rst │ └── util.rst ├── porting.rst ├── restful.rst ├── routes-logo.png ├── setting_up.rst ├── src │ ├── README │ └── routes-logo-boehme.svg ├── todo.rst └── uni_redirect_rest.rst ├── routes ├── __init__.py ├── base.py ├── mapper.py ├── middleware.py ├── route.py └── util.py ├── scripts └── pylintrc ├── setup.cfg ├── setup.py ├── tests ├── test_files │ └── controller_files │ │ ├── admin │ │ └── users.py │ │ ├── content.py │ │ └── users.py ├── test_functional │ ├── __init__.py │ ├── profile_rec.py │ ├── test_explicit_use.py │ ├── test_generation.py │ ├── test_middleware.py │ ├── test_nonminimization.py │ ├── test_recognition.py │ ├── test_resources.py │ ├── test_submapper.py │ └── test_utils.py └── test_units │ ├── test_base.py │ ├── test_environment.py │ ├── test_mapper_str.py │ └── test_route_escapes.py └── tox.ini /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | name: Python package 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | schedule: 9 | - cron: 1 0 * * * # Run daily at 0:01 UTC 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | python-version: 18 | - 2.7 19 | - 3.5 20 | - 3.6 21 | - 3.7 22 | - 3.8 23 | - 3.9 24 | - pypy2 25 | - pypy3 26 | 27 | env: 28 | TOX_PARALLEL_NO_SPINNER: 1 29 | TOXENV: python 30 | 31 | steps: 32 | - uses: actions/checkout@v2 33 | - name: Set up Python ${{ matrix.python-version }} 34 | uses: actions/setup-python@v2 35 | with: 36 | python-version: ${{ matrix.python-version }} 37 | - name: Install dependencies 38 | run: pip install tox 39 | - name: "Initialize tox envs" 40 | run: >- 41 | python -m 42 | tox 43 | --parallel auto 44 | --parallel-live 45 | --notest 46 | --skip-missing-interpreters false 47 | - name: Test with tox 48 | run: >- 49 | python -m 50 | tox 51 | --parallel auto 52 | --parallel-live 53 | -- 54 | -vvvvv 55 | lint: 56 | runs-on: ubuntu-latest 57 | strategy: 58 | matrix: 59 | python-version: 60 | - 3.9 61 | toxenv: 62 | - style 63 | env: 64 | TOX_PARALLEL_NO_SPINNER: 1 65 | TOXENV: ${{ matrix.toxenv }} 66 | steps: 67 | - uses: actions/checkout@v2 68 | - name: "Set up Python ${{ matrix.python-version }}" 69 | uses: actions/setup-python@v2 70 | with: 71 | python-version: ${{ matrix.python-version }} 72 | - name: Install dependencies 73 | run: pip install tox 74 | - name: "Initialize tox envs: ${{ env.TOXENV }}" 75 | run: >- 76 | python -m 77 | tox 78 | --parallel auto 79 | --parallel-live 80 | --notest 81 | --skip-missing-interpreters false 82 | - name: Test with tox 83 | run: >- 84 | python -m 85 | tox 86 | --parallel auto 87 | --parallel-live 88 | -- 89 | -vvvvv 90 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | syntax:glob 3 | .svn 4 | *.pyc 5 | *.egg-info 6 | *.egg 7 | build 8 | dist 9 | docs/_build 10 | *.xml 11 | html_coverage 12 | .hgignore 13 | .idea 14 | *.iml 15 | .tox/ 16 | .eggs/ 17 | .coverage 18 | -------------------------------------------------------------------------------- /.hgignore: -------------------------------------------------------------------------------- 1 | 2 | # Automatically generated by `hgimportsvn` 3 | .DS_Store 4 | syntax:glob 5 | .svn 6 | *.pyc 7 | *.egg-info 8 | *.egg 9 | build 10 | dist 11 | docs/_build 12 | *.xml 13 | html_coverage 14 | .hgignore 15 | -------------------------------------------------------------------------------- /.hgtags: -------------------------------------------------------------------------------- 1 | 3764274a72f21965abb4b2be66599cfd70f8f91a v0.1 2 | f1a022f4db4b732ee126cabd41c5c9b055cb00d5 v0.2 3 | f1a022f4db4b732ee126cabd41c5c9b055cb00d5 v1.0 4 | e8bfc52e772b78d253aedda81bf567e1e22cf889 v0.2 5 | fa170b1f6f38cdb72c667f2b6f17d0c693da09c2 v1.0.1 6 | a42690d130f9b733002111d6259e126145322743 v1.0.2 7 | 1648028ec9659e65394b976b85a294843d0e8b9c v1.1 8 | 5747472c63a3d4743b70f9211c219507383bbcfa v1.2 9 | e1e1a270ef342517cef91677d88fc5c119d6d8b1 v1.3 10 | 83b1236376f07a7371c99b49cbb0e519a27d566a v1.3.1 11 | 600f28b44788b02680523bfed85a3c81e05484a8 v1.3.2 12 | 76169f12a00ceb4f21a761a775c1b2b16eacfcca v1.4 13 | 0a760e3c22fd73ba48977a6e5402f1e9c160d243 v1.4.1 14 | e82016583e820ed9d77f3560a2cbe2ea4bbfce41 v1.5 15 | 0de5d0f46b7a8677680a1524b08bdfdb94924507 v1.5.1 16 | f3cd00b17d42c3dbca8bf094b3d68361763df629 v1.5.2 17 | 5a94e9036ac4ed387b89534de0a34447a148915a v1.6 18 | e5d748a6448a6ecddb556086a9ad67e2cbb98020 v1.6.1 19 | 1c0a390611460a08835adb8f8830a323ba50254f v1.6.2 20 | 287bfd274c78d82db1a56062879087bc506f2035 v1.6.2.1 21 | b1a3cdaf2bd963c3e6432c520f8b47c2f071c2e1 v1.6.3 22 | 8b4164b86f2d201319de1946e75b74280aa7fdf4 v1.7 23 | 8bd901e293224dbe7a4b56908355f08fbc05d972 v1.7.1 24 | 7a1f5bd7d21988ffb4aca1040e7304f58d4418d2 v1.7.3 25 | 2975f052130334fdfc899c3b316f60524ec67042 v1.7.2 26 | d328ecbaedb40495f36218f3a3efdefff8f6d13f v1.8 27 | 42988d6a9d79a1ec02c429fc3a179107f3debcb2 v1.9 28 | 4261583778a6f4e7f7ff9cf41cbc808c8c7428c3 v1.9.1 29 | 79b4b18a45f3a4c7ca993e69f978ca4680a57d85 v1.9.2 30 | 61de888c46cb7c9906ccec869663330288c3bdb9 v1.10 31 | 860993654ac422f53361e6d796ff96bdc8a221f8 v1.10.1 32 | cb61b18ee0a2ec38e90f5e047ebfec635b288e90 v1.10.2 33 | 75ac2bfcb10466420b5c37f6ce7c78264e566dee v1.10.3 34 | bfb8bd725d7f2766a5bc5a9182a01f123dd76099 v1.11 35 | d48c9a0a769f65c7d03382d29c0a80fb035a0fee v1.12 36 | 5cf11614bf01c8fc655ba5f91741ec11122c64ed v1.12.1 37 | 55b90c62a7b698af91f2e43f47e19546389e0148 v1.12.3 38 | 09662e4dc7a277227552ee8fa45b760fe0aba309 v1.13 39 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | Routes Changelog 2 | %%%%%%%%%%%%%%%% 3 | 4 | Release 2.5.1 (October 13, 2020) 5 | ================================ 6 | * Add compatibility for Python 3.7+. PR #99. 7 | 8 | Release 2.5.0 (October 13, 2020) 9 | ================================ 10 | 11 | * Add graceful fallback for invalid character encoding from request object. Patch by Phillip Baker. 12 | PR #94. 13 | * Enhanced performance for matching routes that share the same static prefix. Patch by George Sakkis. 14 | PR #89. 15 | * Fixed issue with child routes not passing route conditions to the Mapper.connect call. Patch by 16 | Robin Abbi. PR #88. 17 | * Fixed documentation to reflect default value for minimization. Patch by Marcin Raczyński. PR #86. 18 | * Allow backslash to escape special characters in route paths. Patch by Orhan Kavrakoğlu. PR #83. 19 | * Resolve invalid escape sequences. Patch by Stephen Finucane. PR #85. 20 | * Remove support for Python 2.6, 3.3, and 3.4. Patch by Stephen Finucane. PR #85. 21 | * Remove obsolete Python 2.3 compat code. Patch by Jakub Wilk. PR #80. 22 | 23 | Release 2.4.1 (January 1, 2017) 24 | =============================== 25 | 26 | * Release as a universal wheel. PR #75. 27 | * Convert readthedocs links for their .org -> .io migration for hosted projects. PR #67. 28 | 29 | Release 2.3.1 (March 30, 2016) 30 | ============================== 31 | * Backwards compatability fix - connect should work with mandatory 32 | routename and optional path. Patch by Davanum Srinivas (PR #65). 33 | 34 | Release 2.3 (March 28, 2016) 35 | ============================ 36 | * Fix sub_domain equivalence check. Patch by Nikita Uvarov 37 | * Add support for protocol-relative URLs generation (i.e. starting with double 38 | slash ``//``). PR #60. Patch by Sviatoslav Sydorenko. 39 | * Add support for the ``middleware`` extra requirement, making possible to 40 | depend on ``webob`` optionally. PR #59. Patch by Sviatoslav Sydorenko. 41 | * Fix matching of an empty string route, which led to exception in earlier 42 | versions. PR #58. Patch by Sviatoslav Sydorenko. 43 | * Add support for the ``requirements`` option when using 44 | mapper.resource to create routes. PR #57. Patch by Sean Dague. 45 | * Concatenation fix when using submappers with path prefixes. Multiple 46 | submappers combined the path prefix inside the controller argument in 47 | non-obvious ways. The controller argument will now be properly carried 48 | through when using submappers. PR #28. 49 | 50 | Release 2.2 (July 21, 2015) 51 | =========================== 52 | * Fix Python 3 support. Patch by Victor Stinner. 53 | 54 | Release 2.1 (January 17, 2015) 55 | ============================== 56 | * Fix 3 other route matching groups in route.py to use anonymous groups for 57 | optional sections to avoid exceeding regex limits. Fixes #15. 58 | * Printing a mapper now includes the Controller/action parameters from the 59 | route. Fixes #11. 60 | * Fix regression that didn't allow passing in params 'host', 'protocol', or 61 | 'anchor'. They can now be passed in with a trailing '_' as was possible 62 | before commit d1d1742903fa5ca24ef848a6ae895303f2661b2a. Fixes #7. 63 | * URL generation with/without SCRIPT_NAME was resulting in the URL cache 64 | failing to return the appropriate cached URL generation. The URL cache 65 | should always include the SCRIPT_NAME, even if its empty, in the cache 66 | to avoid this, and now does. Fixes #6. 67 | * Extract Route creation into separate method in Mapper. Subclasses of Route 68 | can be created by Mappers now. 69 | * Use the first X_FORWARDED_FOR value if there are multiple proxies in the 70 | path. Fixes #5. 71 | 72 | Release 2.0 (November 17, 2013) 73 | =============================== 74 | * Python 3.2/3.3 Support. Fixes Issue #2. Thanks to Alejandro Sánchez for 75 | the pull request! 76 | 77 | Release 1.13 (March 12, 2012) 78 | ============================= 79 | * Fix bug with dots forcing extension by default. The portion with the dot can 80 | now be recognized. Patch by Michael Basnight. 81 | 82 | Release 1.12.3 (June 5, 2010) 83 | ============================= 84 | * Fix bug with URLGenerator not properly including SCRIPT_NAME when generating 85 | URL's and the singleton is not present. 86 | 87 | Release 1.12.2 (May 5, 2010) 88 | ============================ 89 | * Fix bug with routes URLGenerator not properly including SCRIPT_NAME when 90 | generating qualified URL's. 91 | 92 | Release 1.12.1 (March 11, 2010) 93 | =============================== 94 | * Fix bug with routes not generating URL's with callables in defaults. 95 | * Fix bug with routes not handling sub-domain defaults during generation. 96 | 97 | Release 1.12 (February 28, 2010) 98 | ================================ 99 | * Split up the Routes docs. 100 | * Fix bug with relative URL's using qualified merging host and URL without 101 | including the appropriate slash. Fixes #13. 102 | * Fix bug with mapper.extend and Routes modifying their original args. 103 | Fixes #24. 104 | * Fix url.current() not returning current args when explicit is True. 105 | * Added explicit way to directly use the Mapper to match with environ. 106 | * Fix bug with improper len placement for submapper. 107 | * Adding regular expression builder for entire regexp for faster rejection 108 | in a single regexp match should none of the routes match. 109 | * Give Mapper a tabular string representation. 110 | * Make SubMapper objects nestable and add route-generation helpers. 111 | * Add SubMapper-based collections. 112 | * Make the deprecated Mapper.minimization False (disabled) by default. 113 | * Make the mapper explicit (true) by default. 114 | 115 | Release 1.11 (September 28, 2009) 116 | ================================= 117 | * Extensive documentation rewrite. 118 | * Added Mapper.extend function that allows one to add lists of Routes objects 119 | to the mapper in one batch, optionally with a path_prefix. 120 | * Added Mapper.submapper function that returns a SubMapper object to enable 121 | easier declaration of routes that have multiple keyword argument options 122 | in common. 123 | * Mapper controller_scan argument now handles None, and lists of controller 124 | names in addition to a callable. 125 | * Route object now takes a name parameter, which is the name it responds to. 126 | This name is automatically added when called by using Mapper's connect 127 | class method. 128 | * Added optional LRU object for use with Routes when URL's change too often 129 | for the Routes urlcache dict to be a viable option. 130 | 131 | Release 1.10.3 (February 8, 2009) 132 | ================================= 133 | * Tweak to use WebOb Request rather than Paste. 134 | * Performance tweaks for URL recognition. 135 | * Bugfix for routes.middleware not re.escaping the path_info before moving it 136 | to the script name. 137 | 138 | Release 1.10.2 (January 11, 2009) 139 | ================================= 140 | * Bugfix for unicode encoding problems with non-minimized Route generation. 141 | Spotted by Wichert Akkerman. 142 | * Bugfix for when environ is {} in unit tests. 143 | 144 | Release 1.10.1 (September 27, 2008) 145 | =================================== 146 | * Removing LRU cache due to performance and threading issues. Cache does hit 147 | a max-size for the given routes. 148 | 149 | Release 1.10 (September 24, 2008) 150 | ================================= 151 | * Adding LRU cache instead of just dict for caching generated routes. This 152 | avoids slow memory leakage over long-running and non-existent route 153 | generation. 154 | * Adding URLGenerator object. 155 | * Adding redirect routes. 156 | * Static routes can now interpolate variable parts in the path if using {} 157 | variable part syntax. 158 | * Added sub_domain condition option to accept False or None, to require that 159 | there be no sub-domain provided for the route to match. 160 | 161 | Release 1.9.2 (July 8, 2008) 162 | ============================ 163 | * Fixed bug in url_for which caused it to return a literal when it shouldn't 164 | have. 165 | 166 | Release 1.9.1 (June 28, 2008) 167 | ============================= 168 | * Fixed bug in formatted route recognition with formatting being absorbed 169 | into the id. 170 | 171 | Release 1.9 (June 12, 2008) 172 | =========================== 173 | * Fix undefined arg bug in url_for. 174 | * Fixed bug with url_for not working properly outside of a request when 175 | sub-domains are active. Thanks Pavel Skvazh. 176 | * Add non-minimization option to Routes and the Mapper for generation and 177 | recognition. 178 | * Add Routes 2.0 style syntax for making routes and regexp. For example, this 179 | route will now work: '{controller}/{action}/{id}'. 180 | * Fixed Routes to not use quote_plus when making URL's. 181 | * WARNING: Mapper now comes with hardcode_names set to True by default. This 182 | means routes generated by name must work for the URL. 183 | * Actually respect having urlcache disabled. 184 | * WARNING: Calling url_for with a set of args that returns None now throws an 185 | exception. Code that previously checked to see if a url could be made must 186 | be updated accordingly. 187 | * Updated url_for to return url in a literal for use in templating that may 188 | try to escape it again. 189 | * Added option to use X_FORWARDED_PROTO for proxying behind https to work 190 | easier. 191 | * Fixed map.resource to be less restrictive on id than just spaces. 192 | * Fixed Mapper.create_regs not being thread safe, particularly when 193 | always_scan=True. 194 | 195 | Release 1.8 (March 28, 2008) 196 | ============================ 197 | * Fixed bug of map.resource not allowing spaces in id. 198 | * Fixed url generation to properly handle unicode defaults in addition to 199 | unicode arguments. 200 | * Fixed url_for to handle lists as keyword args when generating query 201 | parameters. 202 | * WARNING: Changed map.resource to not use ';', for actions, but the 203 | normal '/'. This means that formatted URL's will also now have the format 204 | come AFTER the action. Ie: /messsages/4.xml;rss -> /messages/4/rss.xml 205 | 206 | Release 1.7.3 (May 28th, 2008) 207 | ============================== 208 | * Fixed triple escaping bug, since WSGI servers are responsible for basic 209 | unescaping. 210 | 211 | Release 1.7.2 (Feb. 27th, 2008) 212 | =============================== 213 | * Fixed bug with keyword args not being coerced to raw string properly. 214 | 215 | Release 1.7.1 (Nov. 16th, 2007) 216 | =============================== 217 | * Fixed bug with sub-domains from route defaults getting encoded to unicode 218 | resulting in a unicode route which then caused url_for to throw an 219 | exception. 220 | * Removed duplicate assignment in map.resource. Patch by Mike Naberezny. 221 | * Applied test patch fix for path checking. Thanks Mike Naberezny. 222 | * Added additional checking of remaining URL, to properly swallow periods in 223 | the appropriate context. Fixes #57. 224 | * Added mapper.hardcode_names option which restricts url generation to the 225 | named route during generation rather than using the routes default options 226 | during generation. 227 | * Fixed the special '_method' attribute not being recognized during POST 228 | requests of Content-Type 'multipart/form-data'. 229 | 230 | Release 1.7 (June 8th, 2007) 231 | ============================ 232 | * Fixed url_unquoting to only apply for strings. 233 | * Added _encoding option to individual routes to toggle decoding/encoding on a 234 | per route basis. 235 | * Fixed route matching so that '.' and other special chars are only part of the 236 | match should they not be followed by that character. Fixed regexp creation so 237 | that route parts with '.' in them aren't matched properly. Fixes #48. 238 | * Fixed Unicode decoding/encoding so that the URL decoding and encoding can be 239 | set on the mapper with mapper.encoding. Fixes #40. 240 | * Don't assume environ['CONTENT_TYPE'] always exists: it may be omitted 241 | according to the WSGI PEP. 242 | * Fixed Unicode decode/encoding of path_info dynamic/wildcard parts so that 243 | PATH_INFO will stay a raw string as it should. Fixes #51. 244 | * Fixed url_for (thus redirect_to) to throw an exception if a Unicode 245 | string is returned as that's an invalid URL. Fixes #46. 246 | * Fixed Routes middleware to only parse POST's if the content type is 247 | application/x-www-form-urlencoded for a HTML form. This properly avoids 248 | parsing wsgi.input when it doesn't need to be. 249 | 250 | Release 1.6.3 (April 10th, 2007) 251 | ================================ 252 | * Fixed matching so that an attempt to match an empty path raises a 253 | RouteException. Fixes #44. 254 | * Added ability to use characters in URL's such as '-' and '_' in 255 | map.resource. Patch by Wyatt Baldwin. Fixes #45. 256 | * Updated Mapper.resource handling with name_prefix and path_prefix checking 257 | to specify defaults. Also ensures that should either of them be set, they 258 | override the prefixes should parent_resource be specified. Patch by Wyatt 259 | Baldwin. Fixes #42. 260 | * Added utf-8 decoding of incoming path arguments, with fallback to ignoring 261 | them in the very rare cases a malformed request URL is sent. Patch from 262 | David Smith. 263 | * Fixed treatment of '#' character as something that can be left off and 264 | used in route paths. Found by Mike Orr. 265 | * Added ability to specify parent resource to map.resource command. Patch from 266 | Wyatt Baldwin. 267 | * Fixed formatted route issue with map.resource when additional collection 268 | methods are specified. Added unit tests to verify the collection methods 269 | work properly. 270 | * Updated URL parsing to properly use HTTP_HOST for hostname + port info before 271 | falling back to SERVER_PORT and SERVER_NAME. Fixes #43. 272 | * Added member_name and collection_name setting to Route object when made with 273 | map.resource. 274 | * Updated routes.middleware to make the Routes matched accessible as 275 | environ['routes.route']. 276 | * Updating mapper object to use thread local for request data (such as 277 | environ) and middleware now deletes environ references at the end of the 278 | request. 279 | * Added explicit option to Routes and Mapper. Routes _explicit setting will 280 | prevent the Route defaults from being implicitly set, while setting Mapper 281 | to explicit will prevent Route implicit defaults and stop url_for from using 282 | Route memory. Fixes #38. 283 | * Updated config object so that the route is attached if possible. 284 | * Adding standard logging usage with debug messages. 285 | * Added additional test for normal '.' match and fixed new special matching to 286 | match it properly. Thanks David Smith. 287 | * Fixed hanging special char issue with 'special' URL chars at the end of a URL 288 | that are missing the variable afterwards. 289 | * Changed Routes generation and recognition to handle other 'special' URL chars 290 | , . and ; as if they were /. This lets them be optionally left out of the 291 | resulting generated URL. Feature requested by David Smith. 292 | * Fixed lookahead assertion in regexp builder to properly handle two grouped 293 | patterns in a row. 294 | * Applied patch to generation and matching to handle Unicode characters 295 | properly. Reported with patch by David Smith. 296 | 297 | Release 1.6.2 (Jan. 5, 2007) 298 | ============================ 299 | * Fixed issue with method checking not properly handling different letter 300 | cases in REQUEST_METHOD. Reported by Sean Davis. 301 | * redirect_to now supports config.redirect returning a redirect, not just 302 | raising one. 303 | 304 | Release 1.6.1 (Dec. 29, 2006) 305 | ============================= 306 | * Fixed zipsafe flag to be False. 307 | 308 | Release 1.6 (Dec. 14th, 2006) 309 | ============================= 310 | * Fixed append_slash to take effect in the route generation itself instead of 311 | relying on url_for function. Reported by ToddG. 312 | * Added additional url_for tests to ensure map.resource generates proper named 313 | routes. 314 | * WARNING: Changed map.resource initialization to accept individual member and 315 | collection names to generate proper singular and plural route names. Those 316 | using map.resource will need to update their routes and url_for statements 317 | accordingly. 318 | * Added additional map.resource recognition tests. 319 | * Added WSGI middleware that does route resolving using new `WSGI.org Routing 320 | Vars Spec `_. 321 | * Added _absolute keyword option route connect to ignore SCRIPT_NAME settings. 322 | Suggested by Ian Bicking. 323 | 324 | Release 1.5.2 (Oct. 16th, 2006) 325 | =============================== 326 | * Fixed qualified keyword to keep host port names when used, unless a host 327 | is specifically passed in. Reported by Jon Rosebaugh. 328 | * Added qualified keyword option to url_for to have it generate a full 329 | URL. Resolves #29. 330 | * Fixed examples in url_for doc strings so they'll be accurate. 331 | 332 | Release 1.5.1 (Oct. 4th, 2006) 333 | ============================== 334 | * Fixed bug with escaping part names in the regular expression, reported by 335 | James Taylor. 336 | 337 | Release 1.5 (Sept. 19th, 2006) 338 | ============================== 339 | * Significant updates to map.resource and unit tests that comb it thoroughly 340 | to ensure its creating all the proper routes (it now is). Increased unit 341 | testing coverage to 95%. 342 | * Added unit tests to ensure controller_scan works properly with nested 343 | controller files and appropriately scans the directory structure. This 344 | brings the Routes util module up to full code coverage. 345 | * Fixed url_for so that when the protocol is changed, port information is 346 | removed from the host. 347 | * Added more thorough testing to _RequestConfig object and the ability to 348 | set your own object. This increases testing coverage of the __init__ module 349 | to 100%. 350 | * Fixed bug with sub_domain not maintaining port information in url_for and 351 | added unit tests. Reported by Jonathan Rosebaugh. 352 | * Added unit tests to ensure sub_domain option works with named routes, cleaned 353 | up url_for memory argument filtering. Fixed bug with named routes and sub_domain 354 | option not working together, reported by Jonathan Rosebaugh. 355 | * Changed order in which sub-domain is added to match-dict so it can be used 356 | in a conditions function. 357 | 358 | Release 1.4.1 (Sept. 6th, 2006) 359 | =============================== 360 | * Added sub_domains option to mapper, along with sub_domains_ignore list for 361 | subdomains that are considered equivilant to the main domain. When sub_domains 362 | is active, url_for will now take a sub_domain option that can alter the host 363 | the route will go to. 364 | * Added ability for filter functions to provide a _host, _protocol, _anchor arg 365 | which is then used to create the URL with the appropriate host/protocol/anchor 366 | destination. 367 | * Patch applied from Ticket #28. Resolves issue with Mapper's controller_scan 368 | function requiring a valid directory argument. Submitted by Zoran Isailovski. 369 | 370 | Release 1.4 (July 21, 2006) 371 | =========================== 372 | * Fixed bug with map.resource related to member methods, found in Rails version. 373 | * Fixed bug with map.resource member methods not requiring a member id. 374 | * Fixed bug related to handling keyword argument controller. 375 | * Added map.resource command which can automatically generate a batch of routes intended 376 | to be used in a REST-ful manner by a web framework. 377 | * Added URL generation handling for a 'method' argument. If 'method' is specified, it 378 | is not dropped and will be changed to '_method' for use by the framework. 379 | * Added conditions option to map.connect. Accepts a dict with optional keyword args 380 | 'method' or 'function'. Method is a list of HTTP methods that are valid for the route. 381 | Function is a function that will be called with environ, matchdict where matchdict is 382 | the dict created by the URL match. 383 | * Fixed redirect_to function for using absolute URL's. redirect_to now passes all args to 384 | url_for, then passes the resulting URL to the redirect function. Reported by climbus. 385 | 386 | Release 1.3.2 (April 30th, 2006) 387 | ================================ 388 | * Fixed _filter bug with inclusion in match dict during matching, reported by David Creemer. 389 | * Fixed improper url quoting by using urllib.encode, patch by Jason Culverhouse. 390 | 391 | Release 1.3.1 (April 4th, 2006) 392 | =============================== 393 | * Mapper has an optional attribute ``append_slash``. When set to ``True``, any URL's 394 | generated will have a slash appended to the end. 395 | * Fixed prefix option so that if the PATH_INFO is empty after prefix regexp, its set to 396 | '/' so the match proceeds ok. 397 | * Fixed prefix bug that caused routes after the initial one to not see the proper url 398 | for matching. Caught by Jochen Kupperschmidt. 399 | 400 | Release 1.3 (Feb. 25th, 2006) 401 | ============================= 402 | * url_for keyword filters: 403 | Named routes can now have a _filter argument that should specify a function that takes 404 | a dict as its sole argument. The dict will contain the full set of keywords passed to 405 | url_for, which the function can then modify as it pleases. The new dict will then be 406 | used as if it was the original set of keyword args given to url_for. 407 | * Fixed Python 2.3 incompatibility due to using keyword arg for a sort statement 408 | when using the built-in controller scanner. 409 | 410 | Release 1.2 (Feb. 17th, 2006) 411 | ============================= 412 | * If a named route doesn't exist, and a url_for call is used, instead of using the 413 | keyword arguments to generate a URL, they will be used as query args for the raw 414 | URL supplied. (Backwards Incompatible) 415 | * If Mapper has debug=True, using match will return two additional values, the route 416 | that matched, if one did match. And a list of routes that were tried, and information 417 | about why they didn't pass. 418 | * url_for enhancements: 419 | Can now be used with 'raw' URL's to generate proper url's for static content that 420 | will then automatically include SCRIPT_NAME if necessary 421 | Static named routes can now be used to shortcut common path information as desired. 422 | * Controller Scanner will now sort controller names so that the longest one is first. This 423 | ensures that the deepest nested controller is executed first before more shallow ones to 424 | increase predictability. 425 | * Controller Scanner now scans directories properly, the version in 1.1 left off the 426 | directory prefix when created the list of controllers. 427 | (Thanks to Justin for drawing my attention to it) 428 | 429 | Release 1.1 (Jan. 13th, 2006) 430 | ============================= 431 | * Routes Mapper additions: 432 | Now takes several optional arguments that determine how it will 433 | generate the regexp's. 434 | Can now hold a function for use when determining what the available 435 | controllers are. Comes with a default directory scanner 436 | Given a directory for the default scanner or a function, the Mapper 437 | will now automatically run it to get the controller list when needed 438 | * Syntax available for splitting routes to allow more complex route paths, such 439 | as ':controller/:(action)-:(id).html' 440 | * Easier setup/integration with Routes per request. Setting the environ in a 441 | WSGI environ will run match, and setup everything needed for url_for/etc. 442 | 443 | Release 1.0.2 (Dec. 30th, 2005) 444 | =============================== 445 | * Routes where a default was present but None were filling in improper values. 446 | * Passing a 0 would evaluate to None during generation, resulting in missing 447 | URL parts 448 | 449 | Release 1.0.1 (Dec. 18th, 2005) 450 | =============================== 451 | * Request Local Callable - You can now designate your own callable function that 452 | should then be used to store the request_config data. This is most useful for 453 | environments where its possible multiple requests might be running in a single 454 | thread. The callable should return a request specific object for attributes to 455 | be attached. See routes.__init__.py for more information. 456 | 457 | Release 1.0 (Nov. 21st, 2005) 458 | ============================= 459 | * routes.__init__ will now load the common symbols most people will 460 | want to actually use. 461 | Thus, you can either:: 462 | 463 | from routes import * 464 | 465 | Or:: 466 | 467 | from routes import request_config, Mapper 468 | 469 | The following names are available for importing from routes:: 470 | 471 | request_config, Mapper, url_for, redirect_to 472 | 473 | * Route Names - You can now name a route, which will save a copy of the defaults 474 | defined for later use by url_for or redirect_to. 475 | Thus, a route and url_for looking like this:: 476 | 477 | m.connect('home', controller='blog', action='splash') 478 | url_for(controller='blog', action='splash') # => /home 479 | 480 | Can now be used with a name:: 481 | 482 | m.connect('home_url','home', controller='blog', action='splash') 483 | url_for('home_url') # => /home 484 | 485 | Additional keywords can still be added to url_for and will override defaults in 486 | the named route. 487 | * Trailing / - Route recognition earlier failed on trailing slashes, not really a bug, 488 | not really a feature I guess. Anyways, trailing slashes are o.k. now as in the Rails 489 | version. 490 | * redirect_to now has two sets of tests to ensure it works properly 491 | -------------------------------------------------------------------------------- /INSTALL: -------------------------------------------------------------------------------- 1 | Installing Routes can be done the traditional way with: 2 | 3 | python setup.py install 4 | 5 | Or directly from the CheeseShop via setuptools: 6 | 7 | easy_install Routes 8 | 9 | Alternatively, you can copy the package to your site-packages directory 10 | to bypass the setuptools based install script (which requires an Internet 11 | connection). 12 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2005-2016 Ben Bangert 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include docs * 2 | include CHANGELOG.rst 3 | include LICENSE.txt 4 | 5 | global-exclude .DS_Store *.hgignore *.hgtags 6 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Routes is a Python re-implementation of the Rails routes system for mapping 2 | URLs to Controllers/Actions and generating URLs. Routes makes it easy to 3 | create pretty and concise URLs that are RESTful with little effort. 4 | 5 | Speedy and dynamic URL generation means you get a URL with minimal cruft 6 | (no big dangling query args). Shortcut features like Named Routes cut down 7 | on repetitive typing. 8 | 9 | See `the documentation for installation and usage of Routes `_. 10 | 11 | .. image:: https://github.com/bbangert/routes/workflows/Python%20package/badge.svg?branch=main&event=push 12 | :target: https://github.com/bbangert/routes/actions?query=workflow%3A%22Python+package%22+branch%3Amain 13 | :alt: GitHub Actions Workflows CI/CD 14 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | Routes TODO 2 | %%%%%%%%%%% 3 | 4 | *Updated 2009-09-07* 5 | 6 | Planned changes 7 | =============== 8 | 9 | Refactoring 10 | ----------- 11 | 12 | Backport the ``Route`` and ``Mapper`` refactorings from Routes-experimental 13 | (formerly called Routes 2). Make the objects more introspection-friendly. 14 | Add a generation dict for named routes; this will help both efficiency and 15 | introspection. 16 | 17 | 18 | Generating the current URL with a modified query string 19 | ------------------------------------------------------- 20 | 21 | When ``url.current()`` generates the current URL, it omits the existing query 22 | string. Any keyword args passed override path variables or set new query 23 | parameters. Extracting the existing query string from the request is tedious, 24 | especially if you want to modify some parameters. 25 | 26 | A new method ``url.current_with_query()`` will generate the current URL with 27 | its query string. Any keyword args add or override query parameters. An 28 | argument with a value ``None`` deletes that parameter if it exists, so that it 29 | will not be in the generated URL. There will be no way to change path 30 | variables in the URL. 31 | 32 | Positional arguments will be appended to the URL path using urljoin. 33 | 34 | Options for generating a fully-qualified URL will be retained. The option 35 | ``_fragment`` specifies a URL fragment ("#fragment"). 36 | 37 | Failure routes 38 | -------------- 39 | 40 | A method ``fail`` for causing 4xx and 5xx errors. This is akin to 41 | ``.redirect`` for 3xx errors. 42 | 43 | A convenience method ``gone`` may also be added for 410 errors. This indicates 44 | that the URL has been deleted and should be removed from bookmarks and 45 | search engines. These will be called "gone routes". 46 | 47 | Chaining to WSGI applications 48 | ----------------------------- 49 | 50 | A connect argument ``wsgi_app`` for chaining to another WSGI application. 51 | This would allow a Pylons app to chain to other applications directly in the 52 | route map rather than having to create dummy controllers for them. 53 | 54 | Users would have to put "{path_info:.*}" at the end of the path to indicate 55 | which part of the URL should be passed to the application. This raises 56 | multiple issues: 57 | 58 | * Users would prefer to specify a URL prefix rather than a URL with a 59 | path_info variable. But this is incompatible with Routes matching. 60 | One could create a special kind of route with a different method, such 61 | as ``map.chain``, but that would raise as many issues as it solves, 62 | such as the need to duplicate all the route options in the second method. 63 | 64 | * What about the sub-application's home page? I.e., PATH_INFO=/ . This 65 | can be handled by changing an empty path_info variable to "/", but what 66 | if the route does not want a path_info variable in the path? 67 | 68 | 69 | New route creation method 70 | ------------------------- 71 | 72 | Add a new mapper method ``add`` with a stricter syntax for creating routes. 73 | (The existing ``connect`` method will remain at least in all 1.x versions.) :: 74 | 75 | map.add(name, path, variables=None, match=True, requirements=None, 76 | if_match=None, if_function=None, if_subdomain=None, if_method=None, 77 | generate_filter=None) 78 | 79 | The first argument, ``name`` is required. It should be a string name, or 80 | ``None`` for unnamed routes. 81 | (This syntax is also allowed by ``connect`` for forward compatibility.) 82 | This eliminates the "moving argument" situation where the ``path`` argument 83 | changes position depending on whether a name is specified. This will make it 84 | easier to read a list of route definitions aligned vertically, encourage named 85 | routes, and make unnamed routes obvious. 86 | 87 | The second argument, ``path``, is unchanged. 88 | 89 | The third argument, ``variables``, is for extra variables. These will be 90 | passed as a dict rather than as keyword args. This will make a clear 91 | distinction between variables and route options, and allow options to have more 92 | intuitive names without risk of collision, and without leading underscores. 93 | New applications can use either the ``{}`` or ``dict()`` syntax; old 94 | applications can simply put ``dict()`` around existing keyword args. If no 95 | extra variables are required you can pass an empty dict, ``None``, or omit the 96 | argument. 97 | 98 | The fourth argument, ``match``, is true if the route is for both matching and 99 | generation, or false for generation only. The default is true. Whea 100 | converting from ``connect``, change ``_static=True`` to ``match=False``. 101 | 102 | The remaining options should be set only via keyword arguments because their 103 | positions might change. 104 | 105 | The ``requirements`` option is unchanged. 106 | 107 | ``if_function`` corresponds to the ``function`` condition in ``connect``. The 108 | value is unchanged. 109 | 110 | ``if_subdomain`` corresponds to the ``subdomain`` condition in ``connect``. 111 | The value is unchanged. 112 | 113 | ``if_method`` corresponds to the ``method`` condition in ``connect``. The 114 | value is unchanged. 115 | 116 | ``generate_filter`` corresponds to the ``filter`` argument to ``connect``. 117 | The value is unchanged. 118 | 119 | One problem is that users might expect this syntax in the ``redirect`` method 120 | (and ``fail`` when it's added), but ``redirect`` can't be changed due to 121 | backward compatibility. Although some of these options may not make sense for 122 | redirect and failure routes anyway. ``fail`` is not so much an issue because 123 | it doesn't exist yet, so it doesn't matter if it's added with the new syntax. 124 | 125 | Resource routes 126 | --------------- 127 | 128 | Add a second kind of resource route with the traditional add-modify-delete 129 | paradigm using only GET and POST, where each GET URL displays a form and the 130 | same POST URL processes it. This is non-RESTful but useful in interactive 131 | applications that don't really need the other methods, and avoids doubling up 132 | dissimilar behavior onto the same URL. The method should also have ``add=True, 133 | edit=True, delete=True`` arguments to disable services which will not be 134 | implemented (e.g., resources that can't be deleted, or are added outside the 135 | web interface). This would be under a different method, hopefully called 136 | something better than ``.resource2``. 137 | 138 | Slimmed-down package 139 | -------------------- 140 | 141 | Make a slimmed-down version of Routes without deprecated features. This can 142 | be kept as a separate branch or repository, and uploaded to PyPI under Routes 143 | with a different filename; e.g., Routes-NC. 144 | 145 | Under consideration 146 | =================== 147 | 148 | Route group 149 | ----------- 150 | 151 | When adding a group of routes such as a resource, keep the group identity for 152 | introspection. Currently the routes are added individually and lose their 153 | groupness. This could be done with a ``RouteGroup`` collection in the matchlist 154 | which delegates to its sub-routes. This would not apply to named generation, 155 | which needs a single dict of route names. 156 | 157 | 158 | Required variables 159 | ------------------ 160 | 161 | A mapper constructor arg listing the variables all 162 | routes must have in their path or extra variables. Defining a route without 163 | these variables would raise an error. Intended for "controller" and "action" 164 | variables in frameworks like Pylons. However, there are cases where 165 | normally-required variables would be omitted, such as chaining to another WSGI 166 | application (in which case "controller" would be necessary but not "action"). 167 | Of course, the route can always define ``action=None``. 168 | -------------------------------------------------------------------------------- /doc-requirements.txt: -------------------------------------------------------------------------------- 1 | -e .[docs] 2 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | 9 | # Internal variables. 10 | PAPEROPT_a4 = -D latex_paper_size=a4 11 | PAPEROPT_letter = -D latex_paper_size=letter 12 | ALLSPHINXOPTS = -d _build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 13 | 14 | .PHONY: help clean html web htmlhelp latex changes linkcheck 15 | 16 | help: 17 | @echo "Please use \`make ' where is one of" 18 | @echo " html to make standalone HTML files" 19 | @echo " web to make files usable by Sphinx.web" 20 | @echo " htmlhelp to make HTML files and a HTML help project" 21 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 22 | @echo " changes to make an overview over all changed/added/deprecated items" 23 | @echo " linkcheck to check all external links for integrity" 24 | 25 | clean: 26 | -rm -rf _build/* 27 | 28 | html: 29 | mkdir -p _build/html _build/doctrees 30 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) _build/html 31 | @echo 32 | @echo "Build finished. The HTML pages are in _build/html." 33 | 34 | web: 35 | mkdir -p _build/web _build/doctrees 36 | $(SPHINXBUILD) -b web $(ALLSPHINXOPTS) _build/web 37 | @echo 38 | @echo "Build finished; now you can run" 39 | @echo " python -m sphinx.web _build/web" 40 | @echo "to start the server." 41 | 42 | htmlhelp: 43 | mkdir -p _build/htmlhelp _build/doctrees 44 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) _build/htmlhelp 45 | @echo 46 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 47 | ".hhp project file in _build/htmlhelp." 48 | 49 | latex: 50 | mkdir -p _build/latex _build/doctrees 51 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) _build/latex 52 | @echo 53 | @echo "Build finished; the LaTeX files are in _build/latex." 54 | @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ 55 | "run these through (pdf)latex." 56 | 57 | changes: 58 | mkdir -p _build/changes _build/doctrees 59 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) _build/changes 60 | @echo 61 | @echo "The overview file is in _build/changes." 62 | 63 | linkcheck: 64 | mkdir -p _build/linkcheck _build/doctrees 65 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) _build/linkcheck 66 | @echo 67 | @echo "Link check complete; look for any errors in the above output " \ 68 | "or in _build/linkcheck/output.txt." 69 | -------------------------------------------------------------------------------- /docs/changes.rst: -------------------------------------------------------------------------------- 1 | :tocdepth: 2 2 | 3 | .. _changes: 4 | 5 | .. include:: ../CHANGELOG.rst 6 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Routes documentation build configuration file, created by 4 | # sphinx-quickstart on Sun Apr 20 19:13:41 2008. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # The contents of this file are pickled, so don't put values in the namespace 9 | # that aren't pickleable (module imports are okay, they're removed automatically). 10 | # 11 | # All configuration values have a default value; values that are commented out 12 | # serve to show the default value. 13 | 14 | import sys, os 15 | 16 | # If your extensions are in another directory, add it here. If the directory 17 | # is relative to the documentation root, use os.path.abspath to make it 18 | # absolute, like shown here. 19 | #sys.path.append(os.path.abspath('.')) 20 | 21 | # General configuration 22 | # --------------------- 23 | 24 | # Add any Sphinx extension module names here, as strings. They can be extensions 25 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 26 | extensions = ['sphinx.ext.autodoc'] 27 | 28 | # Add any paths that contain templates here, relative to this directory. 29 | templates_path = ['_templates'] 30 | 31 | # The suffix of source filenames. 32 | source_suffix = '.rst' 33 | 34 | # The master toctree document. 35 | master_doc = 'index' 36 | 37 | # General substitutions. 38 | project = 'Routes' 39 | copyright = '2005-2020, Ben Bangert, Mike Orr, and numerous contributers' 40 | 41 | # The default replacements for |version| and |release|, also used in various 42 | # other places throughout the built documents. 43 | # 44 | # The short X.Y version. 45 | version = '2.5' 46 | # The full version, including alpha/beta/rc tags. 47 | release = '2.5.0' 48 | 49 | # There are two options for replacing |today|: either, you set today to some 50 | # non-false value, then it is used: 51 | #today = '' 52 | # Else, today_fmt is used as the format for a strftime call. 53 | today_fmt = '%B %d, %Y' 54 | 55 | # List of documents that shouldn't be included in the build. 56 | #unused_docs = [] 57 | 58 | # If true, '()' will be appended to :func: etc. cross-reference text. 59 | #add_function_parentheses = True 60 | 61 | # If true, the current module name will be prepended to all description 62 | # unit titles (such as .. function::). 63 | #add_module_names = True 64 | 65 | # If true, sectionauthor and moduleauthor directives will be shown in the 66 | # output. They are ignored by default. 67 | #show_authors = False 68 | 69 | # The name of the Pygments (syntax highlighting) style to use. 70 | pygments_style = 'sphinx' 71 | 72 | 73 | # Options for HTML output 74 | # ----------------------- 75 | 76 | # The style sheet to use for HTML and HTML Help pages. A file of that name 77 | # must exist either in Sphinx' static/ path, or in one of the custom paths 78 | # given in html_static_path. 79 | # html_style = 'default.css' 80 | 81 | # Add any paths that contain custom static files (such as style sheets) here, 82 | # relative to this directory. They are copied after the builtin static files, 83 | # so a file named "default.css" will overwrite the builtin "default.css". 84 | #html_static_path = ['_static'] 85 | 86 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 87 | # using the given strftime format. 88 | #html_last_updated_fmt = '%b %d, %Y' 89 | 90 | # If true, SmartyPants will be used to convert quotes and dashes to 91 | # typographically correct entities. 92 | #html_use_smartypants = True 93 | 94 | html_theme = 'classic' 95 | html_theme_options = { 96 | "bgcolor": "#fff", 97 | "footertextcolor": "#666", 98 | "relbarbgcolor": "#fff", 99 | "relbarlinkcolor": "#590915", 100 | "relbartextcolor": "#FFAA2D", 101 | "sidebarlinkcolor": "#590915", 102 | "sidebarbgcolor": "#fff", 103 | "sidebartextcolor": "#333", 104 | "footerbgcolor": "#fff", 105 | "linkcolor": "#590915", 106 | "bodyfont": "helvetica, 'bitstream vera sans', sans-serif", 107 | "headfont": "georgia, 'bitstream vera sans serif', 'lucida grande', helvetica, verdana, sans-serif", 108 | "headbgcolor": "#fff", 109 | "headtextcolor": "#12347A", 110 | "codebgcolor": "#fff", 111 | } 112 | 113 | # If false, no module index is generated. 114 | #html_use_modindex = True 115 | 116 | # If false, no index is generated. 117 | #html_use_index = True 118 | 119 | # If true, the index is split into individual pages for each letter. 120 | #html_split_index = False 121 | 122 | # If true, the reST sources are included in the HTML build as _sources/. 123 | #html_copy_source = True 124 | 125 | # If true, an OpenSearch description file will be output, and all pages will 126 | # contain a tag referring to it. The value of this option must be the 127 | # base URL from which the finished HTML is served. 128 | # html_use_opensearch = 'http://routes.groovie.org/' 129 | 130 | # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). 131 | #html_file_suffix = '' 132 | 133 | # Output file base name for HTML help builder. 134 | htmlhelp_basename = 'Routesdoc' 135 | 136 | 137 | # Options for LaTeX output 138 | # ------------------------ 139 | 140 | # The paper size ('letter' or 'a4'). 141 | #latex_paper_size = 'letter' 142 | 143 | # The font size ('10pt', '11pt' or '12pt'). 144 | #latex_font_size = '10pt' 145 | 146 | # Grouping the document tree into LaTeX files. List of tuples 147 | # (source start file, target name, title, author, document class [howto/manual]). 148 | latex_documents = [ 149 | ('contents', 'Routes.tex', u'Routes Documentation', 150 | u'Ben Bangert, Mike Orr', 'manual'), 151 | ] 152 | 153 | # The name of an image file (relative to this directory) to place at the top of 154 | # the title page. 155 | #latex_logo = None 156 | 157 | # For "manual" documents, if this is true, then toplevel headings are parts, 158 | # not chapters. 159 | #latex_use_parts = False 160 | 161 | # Additional stuff for the LaTeX preamble. 162 | latex_preamble = ''' 163 | \usepackage{palatino} 164 | \definecolor{TitleColor}{rgb}{0.7,0,0} 165 | \definecolor{InnerLinkColor}{rgb}{0.7,0,0} 166 | \definecolor{OuterLinkColor}{rgb}{0.8,0,0} 167 | \definecolor{VerbatimColor}{rgb}{0.985,0.985,0.985} 168 | \definecolor{VerbatimBorderColor}{rgb}{0.8,0.8,0.8} 169 | ''' 170 | 171 | # Documents to append as an appendix to all manuals. 172 | #latex_appendices = [] 173 | 174 | # If false, no module index is generated. 175 | latex_use_modindex = False 176 | 177 | # Added to handle docs in middleware.py 178 | autoclass_content = "both" 179 | -------------------------------------------------------------------------------- /docs/generating.rst: -------------------------------------------------------------------------------- 1 | Generation 2 | ========== 3 | 4 | To generate URLs, use the ``url`` or ``url_for`` object provided by your 5 | framework. ``url`` is an instance of Routes ``URLGenerator``, while 6 | ``url_for`` is the older ``routes.url_for()`` function. ``url_for`` is being 7 | phased out, so new applications should use ``url``. 8 | 9 | To generate a named route, specify the route name as a positional argument:: 10 | 11 | url("home") => "/" 12 | 13 | If the route contains path variables, you must specify values for them using 14 | keyword arguments:: 15 | 16 | url("blog", year=2008, month=10, day=2) 17 | 18 | Non-string values are automatically converted to strings using ``str()``. 19 | (This may break with Unicode values containing non-ASCII characters.) 20 | 21 | However, if the route defines an extra variable with the same name as a path 22 | variable, the extra variable is used as the default if that keyword is not 23 | specified. Example:: 24 | 25 | m.connect("archives", "/archives/{id}", 26 | controller="archives", action="view", id=1) 27 | url("archives", id=123) => "/archives/123" 28 | url("archives") => "/archives/1" 29 | 30 | (The extra variable is *not* used for matching unless minimization is enabled.) 31 | 32 | Any keyword args that do not correspond to path variables will be put in the 33 | query string. Append a "_" if the variable name collides with a Python 34 | keyword:: 35 | 36 | map.connect("archive", "/archive/{year}") 37 | url("archive", year=2009, font=large) => "/archive/2009?font=large" 38 | url("archive", year=2009, print_=1) => "/archive/2009?print=1" 39 | 40 | If the application is mounted at a subdirectory of the URL space, 41 | all generated URLs will have the application prefix. The application prefix is 42 | the "SCRIPT_NAME" variable in the request's WSGI environment. 43 | 44 | If the positional argument corresponds to no named route, it is assumed to be a 45 | literal URL. The application's mount point is prefixed to it, and keyword args 46 | are converted to query parameters:: 47 | 48 | url("/search", q="My question") => "/search?q=My+question" 49 | 50 | If there is no positional argument, Routes will use the keyword args to choose 51 | a route. The first route that has all path variables specified by keyword args 52 | and the fewest number of extra variables not overridden by keyword args will be 53 | chosen. This was common in older versions of Routes but can cause application 54 | bugs if an unexpected route is chosen, so using route names is much preferable 55 | because that guarantees only the named route will be chosen. The most common 56 | use for unnamed generation is when you have a seldom-used controller with a lot 57 | of ad hoc methods; e.g., ``url(controller="admin", action="session")``. 58 | 59 | An exception is raised if no route corresponds to the arguments. The exception 60 | is ``routes.util.GenerationException``. (Prior to Routes 1.9, ``None`` was 61 | returned instead. It was changed to an exception to prevent invalid blank URLs 62 | from being insered into templates.) 63 | 64 | You'll also get this exception if Python produces a Unicode URL (which could 65 | happen if the route path or a variable value is Unicode). Routes generates 66 | only ``str`` URLs. 67 | 68 | The following keyword args are special: 69 | 70 | anchor 71 | 72 | Specifies the URL anchor (the part to the right of "#"). :: 73 | 74 | url("home", "summary") => "/#summary" 75 | 76 | host 77 | 78 | Make the URL fully qualified and override the host (domain). 79 | 80 | protocol 81 | 82 | Make the URL fully qualified and override the protocol (e.g., "ftp"). 83 | 84 | qualified 85 | 86 | Make the URL fully qualified (i.e., add "protocol://host:port" prefix). 87 | 88 | sub_domain 89 | 90 | See "Generating URLs with subdomains" below. 91 | 92 | The syntax in this section is the same for both ``url`` and ``url_for``. 93 | 94 | *New in Routes 1.10: ``url`` and the ``URLGenerator`` class behind it.* 95 | 96 | Generating routes based on the current URL 97 | ------------------------------------------ 98 | 99 | ``url.current()`` returns the URL of the current request, without the query 100 | string. This is called "route memory", and works only if the RoutesMiddleware 101 | is in the middleware stack. Keyword arguments override path variables or are 102 | put on the query string. 103 | 104 | ``url_for`` combines the behavior of ``url`` and ``url_current``. This is 105 | deprecated because nameless routes and route memory have the same syntax, which 106 | can lead to the wrong route being chosen in some cases. 107 | 108 | Here's an example of route memory:: 109 | 110 | m.connect("/archives/{year}/{month}/{day}", year=2004) 111 | 112 | # Current URL is "/archives/2005/10/4". 113 | # Routing variables are {"controller": "archives", "action": "view", 114 | "year": "2005", "month": "10", "day": "4"} 115 | 116 | url.current(day=6) => "/archives/2005/10/6" 117 | url.current(month=4) => "/archives/2005/4/4" 118 | url.current() => "/archives/2005/10/4" 119 | 120 | Route memory can be disabled globally with ``map.explicit = True``. 121 | 122 | Generation-only routes (aka. static routes) 123 | ------------------------------------------- 124 | 125 | A static route is used only for generation -- not matching -- and it must be 126 | named. To define a static route, use the argument ``_static=True``. 127 | 128 | This example provides a convenient way to link to a search:: 129 | 130 | map.connect("google", "http://google.com/", _static=True) 131 | url("google", q="search term") => "http://google.com/?q=search+term") 132 | 133 | This example generates a URL to a static image in a Pylons public directory. 134 | Pylons serves the public directory in a way that bypasses Routes, so there's no 135 | reason to match URLs under it. :: 136 | 137 | map.connect("attachment", "/images/attachments/{category}/{id}.jpg", 138 | _static=True) 139 | url("attachment", category="dogs", id="Mastiff") => 140 | "/images/attachments/dogs/Mastiff.jpg" 141 | 142 | Starting in Routes 1.10, static routes are exactly the same as regular routes 143 | except they're not added to the internal match table. In previous versions of 144 | Routes they could not contain path variables and they had to point to external 145 | URLs. 146 | 147 | Filter functions 148 | ---------------- 149 | 150 | A filter function modifies how a named route is generated. Don't confuse it 151 | with a function condition, which is used in matching. A filter function is its 152 | opposite counterpart. 153 | 154 | One use case is when you have a ``story`` object with attributes for year, 155 | month, and day. You don't want to hardcode these attributes in every ``url`` 156 | call because the interface may change someday. Instead you pass the story as a 157 | pseudo-argument, and the filter produces the actual generation args. Here's an 158 | example:: 159 | 160 | class Story(object): 161 | def __init__(self, year, month, day): 162 | self.year = year 163 | self.month = month 164 | self.day = day 165 | 166 | @staticmethod 167 | def expand(kw): 168 | try: 169 | story = kw["story"] 170 | except KeyError: 171 | pass # Don't modify dict if ``story`` key not present. 172 | else: 173 | # Set the actual generation args from the story. 174 | kw["year"] = story.year 175 | kw["month"] = story.month 176 | kw["day"] = story.day 177 | return kw 178 | 179 | m.connect("archives", "/archives/{year}/{month}/{day}", 180 | controller="archives", action="view", _filter=Story.expand) 181 | 182 | my_story = Story(2009, 1, 2) 183 | url("archives", story=my_story) => "/archives/2009/1/2" 184 | 185 | The ``_filter`` argument can be any function that takes a dict and returns a 186 | dict. In the example we've used a static method of the ``Story`` class to keep 187 | everything story-related together, but you may prefer to use a standalone 188 | function to keep Routes-related code away from your model. 189 | 190 | Generating URLs with subdomains 191 | ------------------------------- 192 | 193 | If subdomain support is enabled and the ``sub_domain`` arg is passed to 194 | ``url_for``, Routes ensures the generated route points to that subdomain. :: 195 | 196 | # Enable subdomain support. 197 | map.sub_domains = True 198 | 199 | # Ignore the www subdomain. 200 | map.sub_domains_ignore = "www" 201 | 202 | map.connect("/users/{action}") 203 | 204 | # Add a subdomain. 205 | url_for(action="update", sub_domain="fred") => "http://fred.example.com/users/update" 206 | 207 | # Delete a subdomain. Assume current URL is fred.example.com. 208 | url_for(action="new", sub_domain=None) => "http://example.com/users/new" 209 | -------------------------------------------------------------------------------- /docs/glossary.rst: -------------------------------------------------------------------------------- 1 | .. _glossary: 2 | 3 | Glossary 4 | ======== 5 | 6 | 7 | 8 | .. glossary:: 9 | 10 | component 11 | A part of a URL delimited by slashes. The URL "/help/about" contains 12 | two components: "help" and "about". 13 | 14 | generation 15 | The act of creating a URL based on a route name and/or variable values. 16 | This is the opposite of matching. Finding a route by name is called 17 | *named generation*. Finding a route without specifying a name is 18 | called *nameless generation*. 19 | 20 | mapper 21 | A container for routes. There is normally one mapper per application, 22 | although nested subapplications might have their own mappers. A 23 | mapper knows how to match routes and generate them. 24 | 25 | matching 26 | The act of matching a given URL against a list of routes, and 27 | returning the routing variables. See the *route* entry for an example. 28 | 29 | minimization 30 | A deprecated feature which allowed short URLs to match long paths. 31 | Details are in the ``Backward Compatibility`` section in the manual. 32 | 33 | route 34 | A rule mapping a URL pattern to a dict of routing variables. For 35 | instance, if the pattern is "/{controller}/{action}" and the requested 36 | URL is "/help/about", the resulting dict would be:: 37 | 38 | {"controller": "help", "action": "about"} 39 | 40 | Routes does not know what these variables mean; it simply returns them 41 | to the application. Pylons would look for a ``controllers/help.py`` 42 | module containing a ``HelpController`` class, and call its ``about`` 43 | method. Other frameworks may do something different. 44 | 45 | A route may have a name, used to identify the route. 46 | 47 | route path 48 | The URL pattern in a route. 49 | 50 | routing variables 51 | A dict of key-value pairs returned by matching. Variables defined in 52 | the route path are called *path variables*; their values will be taken 53 | from the URL. Variables defined outside the route path are called 54 | *default variables*; their values are not affected by the URL. 55 | 56 | The WSGI.org environment key for routing variables is 57 | "wsgiorg.routing_args". This manual does not use that term because it 58 | can be confused with function arguments. 59 | 60 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ==================== 2 | Routes Documentation 3 | ==================== 4 | 5 | Routes is a Python re-implementation of the Rails routes system for mapping URLs to application actions, and conversely to generate URLs. Routes makes it easy to create pretty and concise URLs that are RESTful with little effort. 6 | 7 | Routes allows conditional matching based on domain, cookies, HTTP method, or a custom function. Sub-domain support is built in. Routes comes with an extensive unit test suite. 8 | 9 | Current features: 10 | 11 | * Sophisticated route lookup and URL generation 12 | * Named routes 13 | * Redirect routes 14 | * Wildcard paths before and after static parts 15 | * Sub-domain support built-in 16 | * Conditional matching based on domain, cookies, HTTP method (RESTful), and more 17 | * Easily extensible utilizing custom condition functions and route generation 18 | functions 19 | * Extensive unit tests 20 | 21 | Installing 22 | ========== 23 | 24 | Routes can be easily installed with pip or easy_install:: 25 | 26 | $ easy_install routes 27 | 28 | Example 29 | ======= 30 | 31 | .. code-block:: python 32 | 33 | # Setup a mapper 34 | from routes import Mapper 35 | map = Mapper() 36 | map.connect(None, "/error/{action}/{id}", controller="error") 37 | map.connect("home", "/", controller="main", action="index") 38 | 39 | # Match a URL, returns a dict or None if no match 40 | result = map.match('/error/myapp/4') 41 | # result == {'controller': 'error', 'action': 'myapp', 'id': '4'} 42 | 43 | Source 44 | ====== 45 | 46 | The `routes source can be found on GitHub `_. 47 | 48 | Bugs/Support 49 | ============ 50 | 51 | Bug's can be reported on the `github issue tracker 52 | `_. Note that routes is in maintenance 53 | mode so bug reports are unlikely to be worked on, pull requests will be applied 54 | if submitted with tests. 55 | 56 | Documentation 57 | ============= 58 | 59 | .. toctree:: 60 | :maxdepth: 2 61 | 62 | introduction 63 | setting_up 64 | generating 65 | restful 66 | uni_redirect_rest 67 | changes 68 | todo 69 | 70 | .. toctree:: 71 | :maxdepth: 1 72 | 73 | glossary 74 | porting 75 | 76 | 77 | Indices and tables 78 | ================== 79 | 80 | * :ref:`genindex` 81 | * :ref:`modindex` 82 | * :ref:`glossary` 83 | 84 | Module Listing 85 | -------------- 86 | 87 | .. toctree:: 88 | :maxdepth: 2 89 | 90 | modules/index 91 | -------------------------------------------------------------------------------- /docs/introduction.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============ 3 | 4 | Routes tackles an interesting problem that comes up frequently in web 5 | development, *how do you map URLs to your application's actions*? That is, how 6 | do you say that *this* should be accessed as "/blog/2008/01/08", and "/login" 7 | should do *that*? Many web frameworks have a fixed dispatching system; e.g., 8 | "/A/B/C" means to read file "C" in directory "B", or to call method "C" of 9 | class "B" in module "A.B". These work fine until you need to refactor your code 10 | and realize that moving a method changes its public URL and invalidates users' 11 | bookmarks. Likewise, if you want to reorganize your URLs and make a section 12 | into a subsection, you have to change your carefully-tested logic code. 13 | 14 | Routes takes a different approach. You determine your URL hierarchy and 15 | actions separately, and then link them together in whichever ways you decide. 16 | If you change your mind about a particular URL, just change one line in your 17 | route map and never touch your action logic. You can even have multiple URLs 18 | pointing to the same action; e.g., to support legacy bookmarks. Routes was 19 | originally inspired by the dispatcher in Ruby on Rails but has since diverged. 20 | 21 | Routes is the primary dispatching system in the Pylons web framework, and an 22 | optional choice in CherryPy. It can be added to any 23 | framework without much fuss, and used for an entire site or a URL subtree. 24 | It can also forward subtrees to other dispatching systems, which is how 25 | TurboGears 2 is implemented on top of Pylons. 26 | 27 | Current features: 28 | 29 | * Sophisticated route lookup and URL generation 30 | * Named routes 31 | * Redirect routes 32 | * Wildcard paths before and after static parts 33 | * Sub-domain support built-in 34 | * Conditional matching based on domain, cookies, HTTP method (RESTful), and more 35 | * Easily extensible utilizing custom condition functions and route generation 36 | functions 37 | * Extensive unit tests 38 | 39 | Buzzword compliance: REST, DRY. 40 | 41 | If you're new to Routes or have not read the Routes 1.11 manual before, we 42 | recommend reading the `Glossary `_ before continuing. 43 | 44 | This manual is written from the user's perspective: how to use Routes in a 45 | framework that already supports it. The `Porting `_ 46 | manual describes how to add Routes support to a new framework. 47 | -------------------------------------------------------------------------------- /docs/modules/index.rst: -------------------------------------------------------------------------------- 1 | .. _modules: 2 | 3 | ============== 4 | Routes Modules 5 | ============== 6 | 7 | .. toctree:: 8 | :maxdepth: 2 9 | 10 | routes 11 | mapper 12 | route 13 | middleware 14 | util 15 | -------------------------------------------------------------------------------- /docs/modules/mapper.rst: -------------------------------------------------------------------------------- 1 | :mod:`routes.mapper` -- Mapper and Sub-Mapper 2 | ============================================= 3 | 4 | .. automodule:: routes.mapper 5 | 6 | Module Contents 7 | --------------- 8 | 9 | .. autoclass:: SubMapperParent 10 | :members: 11 | :undoc-members: 12 | .. autoclass:: SubMapper 13 | :members: 14 | :undoc-members: 15 | .. autoclass:: Mapper 16 | :members: 17 | -------------------------------------------------------------------------------- /docs/modules/middleware.rst: -------------------------------------------------------------------------------- 1 | :mod:`routes.middleware` -- Routes WSGI Middleware 2 | ================================================== 3 | 4 | .. automodule:: routes.middleware 5 | 6 | Module Contents 7 | --------------- 8 | 9 | .. autoclass:: RoutesMiddleware 10 | :members: 11 | :undoc-members: 12 | .. autofunction:: is_form_post 13 | -------------------------------------------------------------------------------- /docs/modules/route.rst: -------------------------------------------------------------------------------- 1 | :mod:`routes.route` -- Route 2 | ============================ 3 | 4 | .. automodule:: routes.route 5 | 6 | Module Contents 7 | --------------- 8 | 9 | .. autoclass:: Route 10 | :members: 11 | :undoc-members: 12 | -------------------------------------------------------------------------------- /docs/modules/routes.rst: -------------------------------------------------------------------------------- 1 | :mod:`routes` -- Routes Common Classes and Functions 2 | ==================================================== 3 | 4 | .. automodule:: routes 5 | 6 | Module Contents 7 | --------------- 8 | 9 | .. autofunction:: request_config 10 | .. autoclass:: _RequestConfig 11 | :members: 12 | :undoc-members: 13 | -------------------------------------------------------------------------------- /docs/modules/util.rst: -------------------------------------------------------------------------------- 1 | :mod:`routes.util` -- URL Generator and utility functions 2 | ========================================================= 3 | 4 | .. automodule:: routes.util 5 | 6 | Module Contents 7 | --------------- 8 | 9 | .. autoexception:: RoutesException 10 | .. autoexception:: MatchException 11 | .. autoexception:: GenerationException 12 | .. autoclass:: URLGenerator 13 | :members: 14 | :undoc-members: 15 | .. autofunction:: url_for 16 | 17 | .. autofunction:: _url_quote 18 | .. autofunction:: _str_encode 19 | .. autofunction:: _screenargs 20 | .. autofunction:: _subdomain_check 21 | -------------------------------------------------------------------------------- /docs/porting.rst: -------------------------------------------------------------------------------- 1 | Porting Routes to a WSGI Web Framework 2 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 3 | 4 | RoutesMiddleware 5 | ---------------- 6 | 7 | An application can create a raw mapper object and call its ``.match`` and 8 | ``.generate`` methods. However, WSGI applications probably want to use 9 | the ``RoutesMiddleware`` as Pylons does:: 10 | 11 | # In myapp/config/middleware.py 12 | from routes.middleware import RoutesMiddleware 13 | app = RoutesMiddleware(app, map) # ``map`` is a routes.Mapper. 14 | 15 | The middleware matches the requested URL and sets the following WSGI 16 | variables:: 17 | 18 | environ['wsgiorg.routing_args'] = ((url, match)) 19 | environ['routes.route'] = route 20 | environ['routes.url'] = url 21 | 22 | where ``match`` is the routing variables dict, ``route`` is the matched route, 23 | and ``url`` is a ``URLGenerator`` object. In Pylons, ``match`` is used by the 24 | dispatcher, and ``url`` is accessible as ``pylons.url``. 25 | 26 | The middleware handles redirect routes itself, issuing the appropriate 27 | redirect. The application is not called in this case. 28 | 29 | To debug routes, turn on debug logging for the "routes.middleware" logger. 30 | 31 | See the Routes source code for other features which may have been added. 32 | 33 | URL Resolution 34 | -------------- 35 | 36 | When the URL is looked up, it should be matched against the Mapper. When 37 | matching an incoming URL, it is assumed that the URL path is the only string 38 | being matched. All query args should be stripped before matching:: 39 | 40 | m.connect('/articles/{year}/{month}', controller='blog', action='view', year=None) 41 | 42 | m.match('/articles/2003/10') 43 | # {'controller':'blog', 'action':'view', 'year':'2003', 'month':'10'} 44 | 45 | Matching a URL will return a dict of the match results, if you'd like to 46 | differentiate between where the argument came from you can use routematch which 47 | will return the Route object that has all these details:: 48 | 49 | m.connect('/articles/{year}/{month}', controller='blog', action='view', year=None) 50 | 51 | result = m.routematch('/articles/2003/10') 52 | # result is a tuple of the match dict and the Route object 53 | 54 | # result[0] - {'controller':'blog', 'action':'view', 'year':'2003', 'month':'10'} 55 | # result[1] - Route object 56 | # result[1].defaults - {'controller':'blog', 'action':'view', 'year':None} 57 | # result[1].hardcoded - ['controller', 'action'] 58 | 59 | Your integration code is then expected to dispatch to a controller and action 60 | in the dict. How it does this is entirely up to the framework integrator. Your 61 | integration should also typically provide the web developer a mechanism to 62 | access the additional dict values. 63 | 64 | Request Configuration 65 | --------------------- 66 | 67 | If you intend to support ``url_for()`` and ``redirect_to()``, they depend on a 68 | singleton object which requires additional configuration. You're better off 69 | not supporting them at all because they will be deprecated soon. 70 | ``URLGenerator`` is the forward-compatible successor to ``url_for()``. 71 | ``redirect_to()`` is better done in the web framework`as in 72 | ``pylons.controllers.util.redirect_to()``. 73 | 74 | ``url_for()`` and ``redirect_to()`` need information on the current request, 75 | and since they can be called from anywhere they don't have direct access to the 76 | WSGI environment. To remedy this, Routes provides a thread-safe singleton class 77 | called "request_config", which holds the request information for the current 78 | thread. You should update this after matching the incoming URL but before 79 | executing any code that might call the two functions. Here is an example:: 80 | 81 | from routes import request_config 82 | 83 | config = request_config() 84 | 85 | config.mapper = m # Your mapper object 86 | config.mapper_dict = result # The dict from m.match for this URL request 87 | config.host = hostname # The server hostname 88 | config.protocol = port # Protocol used, http, https, etc. 89 | config.redirect = redir_func # A redirect function used by your framework, that is 90 | # expected to take as the first non-keyword arg a single 91 | # full or relative URL 92 | 93 | See the docstring for ``request_config`` in routes/__init__.py to make sure 94 | you've initialized everything necessary. 95 | -------------------------------------------------------------------------------- /docs/restful.rst: -------------------------------------------------------------------------------- 1 | RESTful services 2 | ================ 3 | 4 | Routes makes it easy to configure RESTful web services. ``map.resource`` 5 | creates a set of add/modify/delete routes conforming to the Atom publishing 6 | protocol. 7 | 8 | A resource route addresses *members* in a *collection*, and the collection 9 | itself. Normally a collection is a plural word, and a member is the 10 | corresponding singular word. For instance, consider a collection of messages:: 11 | 12 | map.resource("message", "messages") 13 | 14 | # The above command sets up several routes as if you had typed the 15 | # following commands: 16 | map.connect("messages", "/messages", 17 | controller="messages", action="create", 18 | conditions=dict(method=["POST"])) 19 | map.connect("messages", "/messages", 20 | controller="messages", action="index", 21 | conditions=dict(method=["GET"])) 22 | map.connect("formatted_messages", "/messages.{format}", 23 | controller="messages", action="index", 24 | conditions=dict(method=["GET"])) 25 | map.connect("new_message", "/messages/new", 26 | controller="messages", action="new", 27 | conditions=dict(method=["GET"])) 28 | map.connect("formatted_new_message", "/messages/new.{format}", 29 | controller="messages", action="new", 30 | conditions=dict(method=["GET"])) 31 | map.connect("/messages/{id}", 32 | controller="messages", action="update", 33 | conditions=dict(method=["PUT"])) 34 | map.connect("/messages/{id}", 35 | controller="messages", action="delete", 36 | conditions=dict(method=["DELETE"])) 37 | map.connect("edit_message", "/messages/{id}/edit", 38 | controller="messages", action="edit", 39 | conditions=dict(method=["GET"])) 40 | map.connect("formatted_edit_message", "/messages/{id}.{format}/edit", 41 | controller="messages", action="edit", 42 | conditions=dict(method=["GET"])) 43 | map.connect("message", "/messages/{id}", 44 | controller="messages", action="show", 45 | conditions=dict(method=["GET"])) 46 | map.connect("formatted_message", "/messages/{id}.{format}", 47 | controller="messages", action="show", 48 | conditions=dict(method=["GET"])) 49 | 50 | This establishes the following convention:: 51 | 52 | GET /messages => messages.index() => url("messages") 53 | POST /messages => messages.create() => url("messages") 54 | GET /messages/new => messages.new() => url("new_message") 55 | PUT /messages/1 => messages.update(id) => url("message", id=1) 56 | DELETE /messages/1 => messages.delete(id) => url("message", id=1) 57 | GET /messages/1 => messages.show(id) => url("message", id=1) 58 | GET /messages/1/edit => messages.edit(id) => url("edit_message", id=1) 59 | 60 | .. note:: 61 | 62 | Due to how Routes matches a list of URLs, it has no inherent knowledge of 63 | a route being a **resource**. As such, if a route fails to match due to 64 | the method requirements not being met, a 404 will return just like any 65 | other failure to match a route. 66 | 67 | Thus, you GET the collection to see an index of links to members ("index" 68 | method). You GET a member to see it ("show"). You GET "COLLECTION/new" to 69 | obtain a new message form ("new"), which you POST to the collection ("create"). 70 | You GET "MEMBER/edit" to obtain an edit for ("edit"), which you PUT to the 71 | member ("update"). You DELETE the member to delete it. Note that there are 72 | only four route names because multiple actions are doubled up on the same URLs. 73 | 74 | This URL structure may look strange if you're not used to the Atom protocol. 75 | REST is a vague term, and some people think it means proper URL syntax (every 76 | component contains the one on its right), others think it means not putting IDs 77 | in query parameters, and others think it means using HTTP methods beyond GET 78 | and POST. ``map.resource`` does all three, but it may be overkill for 79 | applications that don't need Atom compliance or prefer to stick with GET and 80 | POST. ``map.resource`` has the advantage that many automated tools and 81 | non-browser agents will be able to list and modify your resources without any 82 | programming on your part. But you don't have to use it if you prefer a simpler 83 | add/modify/delete structure. 84 | 85 | HTML forms can produce only GET and POST requests. As a workaround, if a POST 86 | request contains a ``_method`` parameter, the Routes middleware changes the 87 | HTTP method to whatever the parameter specifies, as if it had been requested 88 | that way in the first place. This convention is becoming increasingly common 89 | in other frameworks. If you're using WebHelpers, the The WebHelpers ``form`` 90 | function has a ``method`` argument which automatically sets the HTTP method and 91 | "_method" parameter. 92 | 93 | Several routes are paired with an identical route containing the ``format`` 94 | variable. The intention is to allow users to obtain different formats by means 95 | of filename suffixes; e.g., "/messages/1.xml". This produces a routing 96 | variable "xml", which in Pylons will be passed to the controller action if it 97 | defines a formal argument for it. In generation you can pass the ``format`` 98 | argument to produce a URL with that suffix:: 99 | 100 | url("message", id=1, format="xml") => "/messages/1.xml" 101 | 102 | Routes does not recognize any particular formats or know which ones are valid 103 | for your application. It merely passes the ``format`` attribute through if it 104 | appears. 105 | 106 | New in Routes 1.7.3: changed URL suffix from ";edit" to "/edit". Semicolons 107 | are not allowed in the path portion of a URL except to delimit path parameters, 108 | which nobody uses. 109 | 110 | Resource options 111 | ---------------- 112 | 113 | The ``map.resource`` method recognizes a number of keyword args which modifies 114 | its behavior: 115 | 116 | controller 117 | 118 | Use the specified controller rather than deducing it from the collection 119 | name. 120 | 121 | collection 122 | 123 | Additional URLs to allow for the collection. Example:: 124 | 125 | map.resource("message", "messages", collection={"rss": "GET"}) 126 | # "GET /message/rss" => ``Messages.rss()``. 127 | # Defines a named route "rss_messages". 128 | 129 | member 130 | 131 | Additional URLs to allow for a member. Example:: 132 | 133 | map.resource('message', 'messages', member={'mark':'POST'}) 134 | # "POST /message/1/mark" => ``Messages.mark(1)`` 135 | # also adds named route "mark_message" 136 | 137 | This can be used to display a delete confirmation form:: 138 | 139 | map.resource("message", "messages", member={"ask_delete": "GET"} 140 | # "GET /message/1/ask_delete" => ``Messages.ask_delete(1)``. 141 | # Also adds a named route "ask_delete_message". 142 | 143 | new 144 | 145 | Additional URLs to allow for new-member functionality. :: 146 | 147 | map.resource("message", "messages", new={"preview": "POST"}) 148 | # "POST /messages/new/preview" 149 | 150 | path_prefix 151 | 152 | Prepend the specified prefix to all URL patterns. The prefix may include 153 | path variables. This is mainly used to nest resources within resources. 154 | 155 | name_prefix 156 | 157 | Prefix the specified string to all route names. This is most often 158 | combined with ``path_prefix`` to nest resources:: 159 | 160 | map.resource("message", "messages", controller="categories", 161 | path_prefix="/category/{category_id}", 162 | name_prefix="category_") 163 | # GET /category/7/message/1 164 | # Adds named route "category_message" 165 | 166 | parent_resource 167 | 168 | A dict containing information about the parent resource, for creating a 169 | nested resource. It should contain the member_name and collection_name 170 | of the parent resource. This dict will be available via the associated 171 | Route object which can be accessed during a request via 172 | ``request.environ["routes.route"]``. 173 | 174 | If parent_resource is supplied and path_prefix isn't, path_prefix will 175 | be generated from parent_resource as "/:_id". 177 | 178 | If parent_resource is supplied and name_prefix isn't, name_prefix will 179 | be generated from parent_resource as "_". 180 | 181 | Example:: 182 | 183 | >>> m = Mapper() 184 | >>> m.resource('location', 'locations', 185 | ... parent_resource=dict(member_name='region', 186 | ... collection_name='regions')) 187 | >>> # path_prefix is "regions/:region_id" 188 | >>> # name prefix is "region_" 189 | >>> url('region_locations', region_id=13) 190 | '/regions/13/locations' 191 | >>> url('region_new_location', region_id=13) 192 | '/regions/13/locations/new' 193 | >>> url('region_location', region_id=13, id=60) 194 | '/regions/13/locations/60' 195 | >>> url('region_edit_location', region_id=13, id=60) 196 | '/regions/13/locations/60/edit' 197 | 198 | Overriding generated path_prefix: 199 | 200 | >>> m = Mapper() 201 | >>> m.resource('location', 'locations', 202 | ... parent_resource=dict(member_name='region', 203 | ... collection_name='regions'), 204 | ... path_prefix='areas/:area_id') 205 | >>> # name prefix is "region_" 206 | >>> url('region_locations', area_id=51) 207 | '/areas/51/locations' 208 | 209 | Overriding generated name_prefix: 210 | 211 | >>> m = Mapper() 212 | >>> m.resource('location', 'locations', 213 | ... parent_resource=dict(member_name='region', 214 | ... collection_name='regions'), 215 | ... name_prefix='') 216 | >>> # path_prefix is "regions/:region_id" 217 | >>> url('locations', region_id=51) 218 | '/regions/51/locations' 219 | -------------------------------------------------------------------------------- /docs/routes-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbangert/routes/7e82bd895a120cbf73f271c32ac289d51124303f/docs/routes-logo.png -------------------------------------------------------------------------------- /docs/setting_up.rst: -------------------------------------------------------------------------------- 1 | Setting up routes 2 | ================= 3 | 4 | It is assumed that you are using a framework that has preconfigured Routes for 5 | you. In Pylons, you define your routes in the ``make_map`` function in your 6 | *myapp/config/routing.py* module. Here is a typical configuration: 7 | 8 | .. code-block:: python 9 | :linenos: 10 | 11 | from routes import Mapper 12 | map = Mapper() 13 | map.connect(None, "/error/{action}/{id}", controller="error") 14 | map.connect("home", "/", controller="main", action="index") 15 | # ADD CUSTOM ROUTES HERE 16 | map.connect(None, "/{controller}/{action}") 17 | map.connect(None, "/{controller}/{action}/{id}") 18 | 19 | Lines 1 and 2 create a mapper. 20 | 21 | Line 3 matches any three-component route that starts with "/error", and sets 22 | the "controller" variable to a constant, so that a URL 23 | "/error/images/arrow.jpg" would produce:: 24 | 25 | {"controller": "error", "action": "images", "id": "arrow.jpg"} 26 | 27 | Line 4 matches the single URL "/", and sets both the controller and action to 28 | constants. It also has a route name "home", which can be used in generation. 29 | (The other routes have ``None`` instead of a name, so they don't have names. 30 | It's recommended to name all routes that may be used in generation, but it's 31 | not necessary to name other routes.) 32 | 33 | Line 6 matches any two-component URL, and line 7 matches any 3-component URL. 34 | These are used as catchall routes if we're too lazy to define a separate route 35 | for every action. If you *have* defined a route for every action, you can 36 | delete these two routes. 37 | 38 | Note that a URL "/error/images/arrow.jpg" could match both line 3 and line 7. 39 | The mapper resolves this by trying routes in the order defined, so this URL 40 | would match line 3. 41 | 42 | If no routes match the URL, the mapper returns a "match failed" condition, 43 | which is seen in Pylons as HTTP 404 "Not Found". 44 | 45 | Here are some more examples of valid routes:: 46 | 47 | m.connect("/feeds/{category}/atom.xml", controller="feeds", action="atom") 48 | m.connect("history", "/archives/by_eon/{century}", controller="archives", 49 | action="aggregate") 50 | m.connect("article", "/article/{section}/{slug}/{page}.html", 51 | controller="article", action="view") 52 | 53 | Extra variables may be any Python type, not just strings. However, if the 54 | route is used in generation, ``str()`` will be called on the value unless 55 | the generation call specifies an overriding value. 56 | 57 | Other argument syntaxes are allowed for compatibility with earlier versions of 58 | Routes. These are described in the ``Backward Compatibility`` section. 59 | 60 | Route paths should always begin with a slash ("/"). Earlier versions of 61 | Routes allowed slashless paths, but their behavior now is undefined. 62 | 63 | 64 | Requirements 65 | ------------ 66 | 67 | It's possible to restrict a path variable to a regular expression; e.g., to 68 | match only a numeric component or a restricted choice of words. There are two 69 | syntaxes for this: inline and the ``requirements`` argument. An inline 70 | requirement looks like this:: 71 | 72 | map.connect(R"/blog/{id:\d+}") 73 | map.connect(R"/download/{platform:windows|mac}/{filename}") 74 | 75 | This matches "/blog/123" but not "/blog/12A". The equivalent ``requirements`` 76 | syntax is:: 77 | 78 | map.connect("/blog/{id}", requirements={"id": R"\d+"} 79 | map.connect("/download/{platform}/{filename}", 80 | requirements={"platform": R"windows|mac"}) 81 | 82 | Note the use of raw string syntax (``R""``) for regexes which might contain 83 | backslashes. Without the R you'd have to double every backslash. 84 | 85 | Another example:: 86 | 87 | m.connect("/archives/{year}/{month}/{day}", controller="archives", 88 | action="view", year=2004, 89 | requirements=dict(year=R"\d{2,4}", month=R"\d{1,2}")) 90 | 91 | The inline syntax was added in Routes (XXX 1.10?? not in changelog). Previous 92 | versions had only the ``requirements`` argument. Two advantages of the 93 | ``requirements`` argument are that if you have several variables with identical 94 | requirements, you can set one variable or even the entire argument to a 95 | global:: 96 | 97 | NUMERIC = R"\d+" 98 | map.connect(..., requirements={"id": NUMERIC}) 99 | 100 | ARTICLE_REQS = {"year": R"\d\d\d\d", "month": R"\d\d", "day": R"\d\d"} 101 | map.connect(..., requirements=ARTICLE_REQS) 102 | 103 | Because the argument ``requirements`` is reserved, you can't define a routing 104 | variable by that name. 105 | 106 | Magic path_info 107 | --------------- 108 | 109 | If the "path_info" variable is used at the end of the URL, Routes moves 110 | everything preceding it into the "SCRIPT_NAME" environment variable. This is 111 | useful when delegating to another WSGI application that does its own routing: 112 | the subapplication will route on the remainder of the URL rather than the 113 | entire URL. You still 114 | need the ":.*" requirement to capture the following URL components into the 115 | variable. :: 116 | 117 | map.connect(None, "/cards/{path_info:.*}", 118 | controller="main", action="cards") 119 | # Incoming URL "/cards/diamonds/4.png" 120 | => {"controller": "main", action: "cards", "path_info": "/diamonds/4.png"} 121 | # Second WSGI application sees: 122 | # SCRIPT_NAME="/cards" PATH_INFO="/diamonds/4.png" 123 | 124 | This route does not match "/cards" because it requires a following slash. 125 | Add another route to get around this:: 126 | 127 | map.connect("cards", "/cards", controller="main", action="cards", 128 | path_info="/") 129 | 130 | .. tip:: 131 | 132 | You may think you can combine the two with the following route:: 133 | 134 | map.connect("cards", "/cards{path_info:.*}", 135 | controller="main", action="cards") 136 | 137 | There are two problems with this, however. One, it would also match 138 | "/cardshark". Two, Routes 1.10 has a bug: it forgets to take 139 | the suffix off the SCRIPT_NAME. 140 | 141 | A future version of Routes may delegate directly to WSGI applications, but for 142 | now this must be done in the framework. In Pylons, you can do this in a 143 | controller action as follows:: 144 | 145 | from paste.fileapp import DirectoryApp 146 | def cards(self, environ, start_response): 147 | app = DirectoryApp("/cards-directory") 148 | return app(environ, start_response) 149 | 150 | Or create a fake controller module with a ``__controller__`` variable set to 151 | the WSGI application:: 152 | 153 | from paste.fileapp import DirectoryApp 154 | __controller__ = DirectoryApp("/cards-directory") 155 | 156 | Conditions 157 | ---------- 158 | 159 | Conditions impose additional constraints on what kinds of requests can match. 160 | The ``conditions`` argument is a dict with up to three keys: 161 | 162 | method 163 | 164 | A list of uppercase HTTP methods. The request must be one of the 165 | listed methods. 166 | 167 | sub_domain 168 | 169 | Can be a list of subdomains, ``True``, ``False``, or ``None``. If a 170 | list, the request must be for one of the specified subdomains. If 171 | ``True``, the request must contain a subdomain but it can be anything. 172 | If ``False`` or ``None``, do not match if there's a subdomain. 173 | 174 | *New in Routes 1.10: ``False`` and ``None`` values.* 175 | 176 | function 177 | 178 | A function that evaluates the request. Its signature must be 179 | ``func(environ, match_dict) => bool``. It should return true if the 180 | match is successful or false otherwise. The first arg is the WSGI 181 | environment; the second is the routing variables that would be 182 | returned if the match succeeds. The function can modify ``match_dict`` 183 | in place to affect which variables are returned. This allows a wide 184 | range of transformations. 185 | 186 | Examples:: 187 | 188 | # Match only if the HTTP method is "GET" or "HEAD". 189 | m.connect("/user/list", controller="user", action="list", 190 | conditions=dict(method=["GET", "HEAD"])) 191 | 192 | # A sub-domain should be present. 193 | m.connect("/", controller="user", action="home", 194 | conditions=dict(sub_domain=True)) 195 | 196 | # Sub-domain should be either "fred" or "george". 197 | m.connect("/", controller="user", action="home", 198 | conditions=dict(sub_domain=["fred", "george"])) 199 | 200 | # Put the referrer into the resulting match dictionary. 201 | # This function always returns true, so it never prevents the match 202 | # from succeeding. 203 | def referals(environ, result): 204 | result["referer"] = environ.get("HTTP_REFERER") 205 | return True 206 | m.connect("/{controller}/{action}/{id}", 207 | conditions=dict(function=referals)) 208 | 209 | Wildcard routes 210 | --------------- 211 | 212 | By default, path variables do not match a slash. This ensures that each 213 | variable will match exactly one component. You can use requirements to 214 | override this:: 215 | 216 | map.connect("/static/{filename:.*?}") 217 | 218 | This matches "/static/foo.jpg", "/static/bar/foo.jpg", etc. 219 | 220 | Beware that careless regexes may eat the entire rest of the URL and cause 221 | components to the right of it not to match:: 222 | 223 | # OK because the following component is static and the regex has a "?". 224 | map.connect("/static/{filename:.*?}/download") 225 | 226 | The lesson is to always test wildcard patterns. 227 | 228 | Format extensions 229 | ----------------- 230 | 231 | A path component of ``{.format}`` will match an optional format extension (e.g. 232 | ".html" or ".json"), setting the format variable to the part after the "." 233 | (e.g. "html" or "json") if there is one, or to ``None`` otherwise. For example:: 234 | 235 | map.connect('/entries/{id}{.format}') 236 | 237 | will match "/entries/1" and "/entries/1.mp3". You can use requirements to 238 | limit which extensions will match, for example:: 239 | 240 | map.connect('/entries/{id:\d+}{.format:json}') 241 | 242 | will match "/entries/1" and "/entries/1.json" but not "/entries/1.mp3". 243 | 244 | As with wildcard routes, it's important to understand and test this. Without 245 | the ``\d+`` requirement on the ``id`` variable above, "/entries/1.mp3" would match 246 | successfully, with the ``id`` variable capturing "1.mp3". 247 | 248 | *New in Routes 1.12.* 249 | 250 | Submappers 251 | ---------- 252 | 253 | A submapper lets you add several similar routes 254 | without having to repeat identical keyword arguments. There are two syntaxes, 255 | one using a Python ``with`` block, and the other avoiding it. :: 256 | 257 | # Using 'with' 258 | with map.submapper(controller="home") as m: 259 | m.connect("home", "/", action="splash") 260 | m.connect("index", "/index", action="index") 261 | 262 | # Not using 'with' 263 | m = map.submapper(controller="home") 264 | m.connect("home", "/", action="splash") 265 | m.connect("index", "/index", action="index") 266 | 267 | # Both of these syntaxes create the following routes:: 268 | # "/" => {"controller": "home", action="splash"} 269 | # "/index" => {"controller": "home", action="index"} 270 | 271 | You can also specify a common path prefix for your routes:: 272 | 273 | with map.submapper(path_prefix="/admin", controller="admin") as m: 274 | m.connect("admin_users", "/users", action="users") 275 | m.connect("admin_databases", "/databases", action="databases") 276 | 277 | # /admin/users => {"controller": "admin", "action": "users"} 278 | # /admin/databases => {"controller": "admin", "action": "databases"} 279 | 280 | All arguments to ``.submapper`` must be keyword arguments. 281 | 282 | The submapper is *not* a complete mapper. It's just a temporary object 283 | with a ``.connect`` method that adds routes to the mapper it was spawned 284 | from. 285 | 286 | *New in Routes 1.11.* 287 | 288 | Submapper helpers 289 | ----------------- 290 | 291 | Submappers contain a number of helpers that further simplify routing 292 | configuration. This:: 293 | 294 | with map.submapper(controller="home") as m: 295 | m.connect("home", "/", action="splash") 296 | m.connect("index", "/index", action="index") 297 | 298 | can be written:: 299 | 300 | with map.submapper(controller="home", path_prefix="/") as m: 301 | m.action("home", action="splash") 302 | m.link("index") 303 | 304 | The ``action`` helper generates a route for one or more HTTP methods ('GET' is 305 | assumed) at the submapper's path ('/' in the example above). The ``link`` 306 | helper generates a route at a relative path. 307 | 308 | There are specific helpers corresponding to the standard ``index``, ``new``, 309 | ``create``, ``show``, ``edit``, ``update`` and ``delete`` actions. 310 | You can use these directly:: 311 | 312 | with map.submapper(controller="entries", path_prefix="/entries") as entries: 313 | entries.index() 314 | with entries.submapper(path_prefix="/{id}") as entry: 315 | entry.show() 316 | 317 | or indirectly:: 318 | 319 | with map.submapper(controller="entries", path_prefix="/entries", 320 | actions=["index"]) as entries: 321 | entries.submapper(path_prefix="/{id}", actions=["show"]) 322 | 323 | Collection/member submappers nested in this way are common enough that there is 324 | helper for this too:: 325 | 326 | map.collection(collection_name="entries", member_name="entry", 327 | controller="entries", 328 | collection_actions=["index"], member_actions["show"]) 329 | 330 | This returns a submapper instance to which further routes may be added; it has 331 | a ``member`` property (a nested submapper) to which which member-specific routes 332 | can be added. When ``collection_actions`` or ``member_actions`` are omitted, 333 | the full set of actions is generated (see the example under "Printing" below). 334 | 335 | See "RESTful services" below for ``map.resource``, a precursor to 336 | ``map.collection`` that does not use submappers. 337 | 338 | *New in Routes 1.12.* 339 | 340 | Adding routes from a nested application 341 | --------------------------------------- 342 | 343 | *New in Routes 1.11.* Sometimes in nested applications, the child application 344 | gives the parent a list of routes to add to its mapper. These can be added 345 | with the ``.extend`` method, optionally providing a path prefix:: 346 | 347 | from routes.route import Route 348 | routes = [ 349 | Route("index", "/index.html", controller="home", action="index"), 350 | ] 351 | 352 | map.extend(routes) 353 | # /index.html => {"controller": "home", "action": "index"} 354 | 355 | map.extend(routes, "/subapp") 356 | # /subapp/index.html => {"controller": "home", "action": "index"} 357 | 358 | This does not exactly add the route objects to the mapper. It creates 359 | identical new route objects and adds those to the mapper. 360 | 361 | *New in Routes 1.11.* 362 | -------------------------------------------------------------------------------- /docs/src/README: -------------------------------------------------------------------------------- 1 | routes-logo-haas.svg 2 | Routes logo by Christoph Boehme, 2009. 3 | -------------------------------------------------------------------------------- /docs/todo.rst: -------------------------------------------------------------------------------- 1 | :tocdepth: 2 2 | 3 | .. _todo: 4 | 5 | .. include:: ../TODO 6 | -------------------------------------------------------------------------------- /docs/uni_redirect_rest.rst: -------------------------------------------------------------------------------- 1 | ============================ 2 | Unicode, Redirects, and More 3 | ============================ 4 | 5 | Unicode 6 | ======= 7 | 8 | Routes assumes UTF-8 encoding on incoming URLs, and ``url`` and ``url_for`` 9 | also generate UTF-8. You can change the encoding with the ``map.charset`` 10 | attribute:: 11 | 12 | map.charset = "latin-1" 13 | 14 | New in Routes 1.10: several bugfixes. 15 | 16 | Redirect Routes 17 | =============== 18 | 19 | Redirect routes allow you to specify redirects in the route map, similar to 20 | RewriteRule in an Apache configuration. This avoids the need to define dummy 21 | controller actions just to handle redirects. It's especially useful when the 22 | URL structure changes and you want to redirect legacy URLs to their new 23 | equivalents. The redirection is done by the Routes middleware, and the WSGI 24 | application is not called. 25 | 26 | ``map.redirect`` takes two positional arguments: the route path and the 27 | destination URL. Redirect routes do not have a name. Both paths can contain 28 | variables, and the route path can take inline requirements. Keyword arguments 29 | are the same as ``map.connect``, both in regards to extra variables and to route 30 | options. :: 31 | 32 | map.redirect("/legacyapp/archives/{url:.*}", "/archives/{url}") 33 | 34 | map.redirect("/legacyapp/archives/{url:.*}", "/archives/{url}") 35 | 36 | By default a "302 Found" HTTP status is issued. You can override this with the 37 | ``_redirect_code`` keyword argument. The value must be an entire status 38 | string. :: 39 | 40 | map.redirect("/home/index", "/", _redirect_code="301 Moved Permanently") 41 | 42 | *New in Routes 1.10.* 43 | 44 | Printing 45 | ======== 46 | 47 | Mappers now have a formatted string representation. In your python shell, 48 | simply print your application's mapper:: 49 | 50 | >>> map.collection("entries", "entry") 51 | >>> print map 52 | Route name Methods Path Controller action 53 | entries GET /entries{.format} entry index 54 | create_entry POST /entries{.format} entry create 55 | new_entry GET /entries/new{.format} entry new 56 | entry GET /entries/{id}{.format} entry show 57 | update_entry PUT /entries/{id}{.format} entry update 58 | delete_entry DELETE /entries/{id}{.format} entry delete 59 | edit_entry GET /entries/{id}/edit{.format} entry edit 60 | 61 | *New in Routes 1.12.* 62 | 63 | *Controller/action fields new in Routes 2.1* 64 | 65 | 66 | Introspection 67 | ============= 68 | 69 | The mapper attribute ``.matchlist`` contains the list of routes to be matched 70 | against incoming URLs. You can iterate this list to see what routes are 71 | defined. This can be useful when debugging route configurations. 72 | 73 | 74 | Other 75 | ===== 76 | 77 | If your application is behind an HTTP proxy such a load balancer on another 78 | host, the WSGI environment will refer to the internal server rather than to the 79 | proxy, which will mess up generated URLs. Use the ProxyMiddleware in 80 | PasteDeploy to fix the WSGI environment to what it would have been without the 81 | proxy. 82 | 83 | To debug routes, turn on debug logging for the "routes.middleware" logger. 84 | (See Python's ``logging`` module to set up your logging configuration.) 85 | 86 | Backward compatibility 87 | ====================== 88 | 89 | The following syntaxes are allowed for compatibility with previous versions 90 | of Routes. They may be removed in the future. 91 | 92 | Omitting the name arg 93 | --------------------- 94 | 95 | In the tutorial we said that nameless routes can be defined by passing ``None`` 96 | as the first argument. You can also omit the first argument entirely:: 97 | 98 | map.connect(None, "/{controller}/{action}") 99 | map.connect("/{controller}/{action}") 100 | 101 | The syntax with ``None`` is preferred to be forward-compatible with future 102 | versions of Routes. It avoids the path argument changing position between 103 | the first and second arguments, which is unpythonic. 104 | 105 | :varname 106 | -------- 107 | 108 | Path variables were defined in the format ``:varname`` and ``:(varname)`` 109 | prior to Routes 1.9. The form with parentheses was called "grouping", used 110 | to delimit the variable name from a following letter or number. Thus the old 111 | syntax "/:controller/:(id)abc" corresponds to the new syntax 112 | "/{controller}/{id}abc". 113 | 114 | The older wildcard syntax is ``*varname`` or ``*(varname)``:: 115 | 116 | # OK because the following component is static. 117 | map.connect("/static/*filename/download") 118 | 119 | # Deprecated syntax. WRONG because the wildcard will eat the rest of the 120 | # URL, leaving nothing for the following variable, which will cause the 121 | # match to fail. 122 | map.connect("/static/*filename/:action") 123 | 124 | 125 | Minimization 126 | ------------ 127 | 128 | Minimization was a misfeature which was intended to save typing, but which 129 | often resulted in the wrong route being chosen. Old applications that still 130 | depend on it must now enable it by putting ``map.minimization = True`` in 131 | their route definitions. 132 | 133 | Without minimization, the URL must contain values for all path variables in 134 | the route:: 135 | 136 | map.connect("basic", "/{controller}/{action}", 137 | controller="mycontroller", action="myaction", weather="sunny") 138 | 139 | This route matches any two-component URL, for instance "/help/about". The 140 | resulting routing variables would be:: 141 | 142 | {"controller": "help", "action": "about", "weather": "sunny"} 143 | 144 | The path variables are taken from the URL, and any extra variables are added as 145 | constants. The extra variables for "controller" and "action" are *never used* 146 | in matching, but are available as default values for generation:: 147 | 148 | url("basic", controller="help") => "/help/about?weather=sunny" 149 | 150 | With minimization, the same route path would also match shorter URLs such as 151 | "/help", "/foo", and "/". Missing values on the right of the URL would be 152 | taken from the extra variables. This was intended to lessen the number of 153 | routes you had to write. In practice it led to obscure application bugs 154 | because sometimes an unexpected route would be matched. Thus Routes 1.9 155 | introduced non-minimization and recommended "map.minimization = False" for 156 | all new applications. 157 | 158 | A corollary problem was generating the wrong route. Routes 1.9 tightened up 159 | the rule for generating named routes. If a route name is specified in 160 | ``url()`` or ``url_for()``, *only* that named route will be chosen. In 161 | previous versions, it might choose another route based on the keyword args. 162 | 163 | Implicit defaults and route memory 164 | ---------------------------------- 165 | 166 | Implicit defaults worked with minimization to provide automatic default values 167 | for the "action" and "id" variables. If a route was defined as 168 | ``map.connect("/{controller}/{action}/{id}") and the URL "/archives"`` was 169 | requested, Routes would implicitly add ``action="index", id=None`` to the 170 | routing variables. 171 | 172 | To enable implicit defaults, set ``map.minimization = True; map.explicit = 173 | False``. You can also enable implicit defaults on a per-route basis by setting 174 | ``map.explicit = True`` and defining each route with a keyword argument ``explicit=False``. 175 | 176 | Previous versions also had implicit default values for "controller", 177 | "action", and "id". These are now disabled by default, but can be enabled via 178 | ``map.explicit = True``. This also enables route memory 179 | 180 | url_for() 181 | --------- 182 | 183 | ``url_for`` was a route generation function which was replaced by the ``url`` 184 | object. Usage is the same except that ``url_for`` uses route memory in some 185 | cases and ``url`` never does. Route memory is where variables from the current 186 | URL (the current request) are injected into the generated URL. To use route 187 | memory with ``url``, call ``url.current()`` passing the variables you want to 188 | override. Any other variables needed by the route will be taken from the 189 | current routing variables. 190 | 191 | In other words, ``url_for`` combines ``url`` and ``url.current()`` into one 192 | function. The location of ``url_for`` is also different. ``url_for`` is 193 | properly imported from ``routes``:: 194 | 195 | from routes import url_for 196 | 197 | ``url_for`` was traditionally imported into WebHelpers, and it's still used in 198 | some tests and in ``webhelpers.paginate``. Many old Pylons applications 199 | contain ``h.url_for()`` based on its traditional importation to helpers.py. 200 | However, its use in new applications is discouraged both because of its 201 | ambiguous syntax and because its implementation depends on an ugly singleton. 202 | 203 | The ``url`` object is created by the RoutesMiddleware and inserted into the 204 | WSGI environment. Pylons makes it available as ``pylons.url``, and in 205 | templates as ``url``. 206 | 207 | redirect_to() 208 | ------------- 209 | 210 | This combined ``url_for`` with a redirect. Instead, please use your 211 | framework's redirect mechanism with a ``url`` call. For instance in Pylons:: 212 | 213 | from pylons.controllers.util import redirect 214 | redirect(url("login")) 215 | -------------------------------------------------------------------------------- /routes/__init__.py: -------------------------------------------------------------------------------- 1 | """Provides common classes and functions most users will want access to.""" 2 | import threading 3 | 4 | 5 | class _RequestConfig(object): 6 | """ 7 | RequestConfig thread-local singleton 8 | 9 | The Routes RequestConfig object is a thread-local singleton that should 10 | be initialized by the web framework that is utilizing Routes. 11 | """ 12 | __shared_state = threading.local() 13 | 14 | def __getattr__(self, name): 15 | return getattr(self.__shared_state, name) 16 | 17 | def __setattr__(self, name, value): 18 | """ 19 | If the name is environ, load the wsgi envion with load_wsgi_environ 20 | and set the environ 21 | """ 22 | if name == 'environ': 23 | self.load_wsgi_environ(value) 24 | return self.__shared_state.__setattr__(name, value) 25 | return self.__shared_state.__setattr__(name, value) 26 | 27 | def __delattr__(self, name): 28 | delattr(self.__shared_state, name) 29 | 30 | def load_wsgi_environ(self, environ): 31 | """ 32 | Load the protocol/server info from the environ and store it. 33 | Also, match the incoming URL if there's already a mapper, and 34 | store the resulting match dict in mapper_dict. 35 | """ 36 | if 'HTTPS' in environ or environ.get('wsgi.url_scheme') == 'https' \ 37 | or environ.get('HTTP_X_FORWARDED_PROTO') == 'https': 38 | self.__shared_state.protocol = 'https' 39 | else: 40 | self.__shared_state.protocol = 'http' 41 | try: 42 | self.mapper.environ = environ 43 | except AttributeError: 44 | pass 45 | 46 | # Wrap in try/except as common case is that there is a mapper 47 | # attached to self 48 | try: 49 | if 'PATH_INFO' in environ: 50 | mapper = self.mapper 51 | path = environ['PATH_INFO'] 52 | result = mapper.routematch(path) 53 | if result is not None: 54 | self.__shared_state.mapper_dict = result[0] 55 | self.__shared_state.route = result[1] 56 | else: 57 | self.__shared_state.mapper_dict = None 58 | self.__shared_state.route = None 59 | except AttributeError: 60 | pass 61 | 62 | if 'HTTP_X_FORWARDED_HOST' in environ: 63 | # Apache will add multiple comma separated values to 64 | # X-Forwarded-Host if there are multiple reverse proxies 65 | self.__shared_state.host = \ 66 | environ['HTTP_X_FORWARDED_HOST'].split(', ', 1)[0] 67 | elif 'HTTP_HOST' in environ: 68 | self.__shared_state.host = environ['HTTP_HOST'] 69 | else: 70 | self.__shared_state.host = environ['SERVER_NAME'] 71 | if environ['wsgi.url_scheme'] == 'https': 72 | if environ['SERVER_PORT'] != '443': 73 | self.__shared_state.host += ':' + environ['SERVER_PORT'] 74 | else: 75 | if environ['SERVER_PORT'] != '80': 76 | self.__shared_state.host += ':' + environ['SERVER_PORT'] 77 | 78 | 79 | def request_config(original=False): 80 | """ 81 | Returns the Routes RequestConfig object. 82 | 83 | To get the Routes RequestConfig: 84 | 85 | >>> from routes import * 86 | >>> config = request_config() 87 | 88 | The following attributes must be set on the config object every request: 89 | 90 | mapper 91 | mapper should be a Mapper instance thats ready for use 92 | host 93 | host is the hostname of the webapp 94 | protocol 95 | protocol is the protocol of the current request 96 | mapper_dict 97 | mapper_dict should be the dict returned by mapper.match() 98 | redirect 99 | redirect should be a function that issues a redirect, 100 | and takes a url as the sole argument 101 | prefix (optional) 102 | Set if the application is moved under a URL prefix. Prefix 103 | will be stripped before matching, and prepended on generation 104 | environ (optional) 105 | Set to the WSGI environ for automatic prefix support if the 106 | webapp is underneath a 'SCRIPT_NAME' 107 | 108 | Setting the environ will use information in environ to try and 109 | populate the host/protocol/mapper_dict options if you've already 110 | set a mapper. 111 | 112 | **Using your own requst local** 113 | 114 | If you have your own request local object that you'd like to use instead 115 | of the default thread local provided by Routes, you can configure Routes 116 | to use it:: 117 | 118 | from routes import request_config() 119 | config = request_config() 120 | if hasattr(config, 'using_request_local'): 121 | config.request_local = YourLocalCallable 122 | config = request_config() 123 | 124 | Once you have configured request_config, its advisable you retrieve it 125 | again to get the object you wanted. The variable you assign to 126 | request_local is assumed to be a callable that will get the local config 127 | object you wish. 128 | 129 | This example tests for the presence of the 'using_request_local' attribute 130 | which will be present if you haven't assigned it yet. This way you can 131 | avoid repeat assignments of the request specific callable. 132 | 133 | Should you want the original object, perhaps to change the callable its 134 | using or stop this behavior, call request_config(original=True). 135 | """ 136 | obj = _RequestConfig() 137 | try: 138 | if obj.request_local and original is False: 139 | return getattr(obj, 'request_local')() 140 | except AttributeError: 141 | obj.request_local = False 142 | obj.using_request_local = False 143 | return _RequestConfig() 144 | 145 | from routes.mapper import Mapper 146 | from routes.util import redirect_to, url_for, URLGenerator 147 | 148 | __all__ = ['Mapper', 'url_for', 'URLGenerator', 'redirect_to', 'request_config'] 149 | -------------------------------------------------------------------------------- /routes/base.py: -------------------------------------------------------------------------------- 1 | """Route and Mapper core classes""" 2 | from routes import request_config 3 | from routes.mapper import Mapper 4 | from routes.route import Route 5 | -------------------------------------------------------------------------------- /routes/middleware.py: -------------------------------------------------------------------------------- 1 | """Routes WSGI Middleware""" 2 | import re 3 | import logging 4 | 5 | from webob import Request 6 | 7 | from routes.base import request_config 8 | from routes.util import URLGenerator 9 | 10 | log = logging.getLogger('routes.middleware') 11 | 12 | 13 | class RoutesMiddleware(object): 14 | """Routing middleware that handles resolving the PATH_INFO in 15 | addition to optionally recognizing method overriding. 16 | 17 | .. Note:: 18 | This module requires webob to be installed. To depend on it, you may 19 | list routes[middleware] in your ``requirements.txt`` 20 | """ 21 | def __init__(self, wsgi_app, mapper, use_method_override=True, 22 | path_info=True, singleton=True): 23 | """Create a Route middleware object 24 | 25 | Using the use_method_override keyword will require Paste to be 26 | installed, and your application should use Paste's WSGIRequest 27 | object as it will properly handle POST issues with wsgi.input 28 | should Routes check it. 29 | 30 | If path_info is True, then should a route var contain 31 | path_info, the SCRIPT_NAME and PATH_INFO will be altered 32 | accordingly. This should be used with routes like: 33 | 34 | .. code-block:: python 35 | 36 | map.connect('blog/*path_info', controller='blog', path_info='') 37 | 38 | """ 39 | self.app = wsgi_app 40 | self.mapper = mapper 41 | self.singleton = singleton 42 | self.use_method_override = use_method_override 43 | self.path_info = path_info 44 | self.log_debug = logging.DEBUG >= log.getEffectiveLevel() 45 | if self.log_debug: 46 | log.debug("Initialized with method overriding = %s, and path " 47 | "info altering = %s", use_method_override, path_info) 48 | 49 | def __call__(self, environ, start_response): 50 | """Resolves the URL in PATH_INFO, and uses wsgi.routing_args 51 | to pass on URL resolver results.""" 52 | old_method = None 53 | if self.use_method_override: 54 | req = None 55 | 56 | # In some odd cases, there's no query string 57 | try: 58 | qs = environ['QUERY_STRING'] 59 | except KeyError: 60 | qs = '' 61 | if '_method' in qs: 62 | req = Request(environ) 63 | req.errors = 'ignore' 64 | 65 | try: 66 | method = req.GET.get('_method') 67 | except UnicodeDecodeError: 68 | method = None 69 | 70 | if method: 71 | old_method = environ['REQUEST_METHOD'] 72 | environ['REQUEST_METHOD'] = method.upper() 73 | if self.log_debug: 74 | log.debug("_method found in QUERY_STRING, altering " 75 | "request method to %s", 76 | environ['REQUEST_METHOD']) 77 | elif environ['REQUEST_METHOD'] == 'POST' and is_form_post(environ): 78 | if req is None: 79 | req = Request(environ) 80 | req.errors = 'ignore' 81 | 82 | try: 83 | method = req.POST.get('_method') 84 | except UnicodeDecodeError: 85 | method = None 86 | 87 | if method: 88 | old_method = environ['REQUEST_METHOD'] 89 | environ['REQUEST_METHOD'] = method.upper() 90 | if self.log_debug: 91 | log.debug("_method found in POST data, altering " 92 | "request method to %s", 93 | environ['REQUEST_METHOD']) 94 | 95 | # Run the actual route matching 96 | # -- Assignment of environ to config triggers route matching 97 | if self.singleton: 98 | config = request_config() 99 | config.mapper = self.mapper 100 | config.environ = environ 101 | match = config.mapper_dict 102 | route = config.route 103 | else: 104 | results = self.mapper.routematch(environ=environ) 105 | if results: 106 | match, route = results[0], results[1] 107 | else: 108 | match = route = None 109 | 110 | if old_method: 111 | environ['REQUEST_METHOD'] = old_method 112 | 113 | if not match: 114 | match = {} 115 | if self.log_debug: 116 | urlinfo = "%s %s" % (environ['REQUEST_METHOD'], 117 | environ['PATH_INFO']) 118 | log.debug("No route matched for %s", urlinfo) 119 | elif self.log_debug: 120 | urlinfo = "%s %s" % (environ['REQUEST_METHOD'], 121 | environ['PATH_INFO']) 122 | log.debug("Matched %s", urlinfo) 123 | log.debug("Route path: '%s', defaults: %s", route.routepath, 124 | route.defaults) 125 | log.debug("Match dict: %s", match) 126 | 127 | url = URLGenerator(self.mapper, environ) 128 | environ['wsgiorg.routing_args'] = ((url), match) 129 | environ['routes.route'] = route 130 | environ['routes.url'] = url 131 | 132 | if route and route.redirect: 133 | route_name = '_redirect_%s' % id(route) 134 | location = url(route_name, **match) 135 | log.debug("Using redirect route, redirect to '%s' with status" 136 | "code: %s", location, route.redirect_status) 137 | start_response(route.redirect_status, 138 | [('Content-Type', 'text/plain; charset=utf8'), 139 | ('Location', location)]) 140 | return [] 141 | 142 | # If the route included a path_info attribute and it should be used to 143 | # alter the environ, we'll pull it out 144 | if self.path_info and 'path_info' in match: 145 | oldpath = environ['PATH_INFO'] 146 | newpath = match.get('path_info') or '' 147 | environ['PATH_INFO'] = newpath 148 | if not environ['PATH_INFO'].startswith('/'): 149 | environ['PATH_INFO'] = '/' + environ['PATH_INFO'] 150 | environ['SCRIPT_NAME'] += re.sub( 151 | r'^(.*?)/' + re.escape(newpath) + '$', r'\1', oldpath) 152 | 153 | response = self.app(environ, start_response) 154 | 155 | # Wrapped in try as in rare cases the attribute will be gone already 156 | try: 157 | del self.mapper.environ 158 | except AttributeError: 159 | pass 160 | return response 161 | 162 | 163 | def is_form_post(environ): 164 | """Determine whether the request is a POSTed html form""" 165 | content_type = environ.get('CONTENT_TYPE', '').lower() 166 | if ';' in content_type: 167 | content_type = content_type.split(';', 1)[0] 168 | return content_type in ('application/x-www-form-urlencoded', 169 | 'multipart/form-data') 170 | -------------------------------------------------------------------------------- /routes/util.py: -------------------------------------------------------------------------------- 1 | """Utility functions for use in templates / controllers 2 | 3 | *PLEASE NOTE*: Many of these functions expect an initialized RequestConfig 4 | object. This is expected to have been initialized for EACH REQUEST by the web 5 | framework. 6 | 7 | """ 8 | import os 9 | import re 10 | 11 | import six 12 | from six.moves import urllib 13 | 14 | from routes import request_config 15 | 16 | 17 | class RoutesException(Exception): 18 | """Tossed during Route exceptions""" 19 | 20 | 21 | class MatchException(RoutesException): 22 | """Tossed during URL matching exceptions""" 23 | 24 | 25 | class GenerationException(RoutesException): 26 | """Tossed during URL generation exceptions""" 27 | 28 | 29 | def _screenargs(kargs, mapper, environ, force_explicit=False): 30 | """ 31 | Private function that takes a dict, and screens it against the current 32 | request dict to determine what the dict should look like that is used. 33 | This is responsible for the requests "memory" of the current. 34 | """ 35 | # Coerce any unicode args with the encoding 36 | encoding = mapper.encoding 37 | for key, val in six.iteritems(kargs): 38 | if isinstance(val, six.text_type): 39 | kargs[key] = val.encode(encoding) 40 | 41 | if mapper.explicit and mapper.sub_domains and not force_explicit: 42 | return _subdomain_check(kargs, mapper, environ) 43 | elif mapper.explicit and not force_explicit: 44 | return kargs 45 | 46 | controller_name = as_unicode(kargs.get('controller'), encoding) 47 | 48 | if controller_name and controller_name.startswith('/'): 49 | # If the controller name starts with '/', ignore route memory 50 | kargs['controller'] = kargs['controller'][1:] 51 | return kargs 52 | elif controller_name and 'action' not in kargs: 53 | # Fill in an action if we don't have one, but have a controller 54 | kargs['action'] = 'index' 55 | 56 | route_args = environ.get('wsgiorg.routing_args') 57 | if route_args: 58 | memory_kargs = route_args[1].copy() 59 | else: 60 | memory_kargs = {} 61 | 62 | # Remove keys from memory and kargs if kargs has them as None 63 | empty_keys = [key for key, value in six.iteritems(kargs) if value is None] 64 | for key in empty_keys: 65 | del kargs[key] 66 | memory_kargs.pop(key, None) 67 | 68 | # Merge the new args on top of the memory args 69 | memory_kargs.update(kargs) 70 | 71 | # Setup a sub-domain if applicable 72 | if mapper.sub_domains: 73 | memory_kargs = _subdomain_check(memory_kargs, mapper, environ) 74 | return memory_kargs 75 | 76 | 77 | def _subdomain_check(kargs, mapper, environ): 78 | """Screen the kargs for a subdomain and alter it appropriately depending 79 | on the current subdomain or lack therof.""" 80 | if mapper.sub_domains: 81 | subdomain = kargs.pop('sub_domain', None) 82 | if isinstance(subdomain, six.text_type): 83 | subdomain = str(subdomain) 84 | 85 | fullhost = environ.get('HTTP_HOST') or environ.get('SERVER_NAME') 86 | 87 | # In case environ defaulted to {} 88 | if not fullhost: 89 | return kargs 90 | 91 | hostmatch = fullhost.split(':') 92 | host = hostmatch[0] 93 | port = '' 94 | if len(hostmatch) > 1: 95 | port += ':' + hostmatch[1] 96 | 97 | match = re.match(r'^(.+?)\.(%s)$' % mapper.domain_match, host) 98 | host_subdomain, domain = match.groups() if match else (None, host) 99 | 100 | subdomain = as_unicode(subdomain, mapper.encoding) 101 | if subdomain and host_subdomain != subdomain and \ 102 | subdomain not in mapper.sub_domains_ignore: 103 | kargs['_host'] = subdomain + '.' + domain + port 104 | elif (subdomain in mapper.sub_domains_ignore or \ 105 | subdomain is None) and domain != host: 106 | kargs['_host'] = domain + port 107 | return kargs 108 | else: 109 | return kargs 110 | 111 | 112 | def _url_quote(string, encoding): 113 | """A Unicode handling version of urllib.quote.""" 114 | if encoding: 115 | if isinstance(string, six.text_type): 116 | s = string.encode(encoding) 117 | elif isinstance(string, six.text_type): 118 | # assume the encoding is already correct 119 | s = string 120 | else: 121 | s = six.text_type(string).encode(encoding) 122 | else: 123 | s = str(string) 124 | return urllib.parse.quote(s, '/') 125 | 126 | 127 | def _str_encode(string, encoding): 128 | if encoding: 129 | if isinstance(string, six.text_type): 130 | s = string.encode(encoding) 131 | elif isinstance(string, six.text_type): 132 | # assume the encoding is already correct 133 | s = string 134 | else: 135 | s = six.text_type(string).encode(encoding) 136 | return s 137 | 138 | 139 | def url_for(*args, **kargs): 140 | """Generates a URL 141 | 142 | All keys given to url_for are sent to the Routes Mapper instance for 143 | generation except for:: 144 | 145 | anchor specified the anchor name to be appened to the path 146 | host overrides the default (current) host if provided 147 | protocol overrides the default (current) protocol if provided 148 | qualified creates the URL with the host/port information as 149 | needed 150 | 151 | The URL is generated based on the rest of the keys. When generating a new 152 | URL, values will be used from the current request's parameters (if 153 | present). The following rules are used to determine when and how to keep 154 | the current requests parameters: 155 | 156 | * If the controller is present and begins with '/', no defaults are used 157 | * If the controller is changed, action is set to 'index' unless otherwise 158 | specified 159 | 160 | For example, if the current request yielded a dict of 161 | {'controller': 'blog', 'action': 'view', 'id': 2}, with the standard 162 | ':controller/:action/:id' route, you'd get the following results:: 163 | 164 | url_for(id=4) => '/blog/view/4', 165 | url_for(controller='/admin') => '/admin', 166 | url_for(controller='admin') => '/admin/view/2' 167 | url_for(action='edit') => '/blog/edit/2', 168 | url_for(action='list', id=None) => '/blog/list' 169 | 170 | **Static and Named Routes** 171 | 172 | If there is a string present as the first argument, a lookup is done 173 | against the named routes table to see if there's any matching routes. The 174 | keyword defaults used with static routes will be sent in as GET query 175 | arg's if a route matches. 176 | 177 | If no route by that name is found, the string is assumed to be a raw URL. 178 | Should the raw URL begin with ``/`` then appropriate SCRIPT_NAME data will 179 | be added if present, otherwise the string will be used as the url with 180 | keyword args becoming GET query args. 181 | 182 | """ 183 | anchor = kargs.get('anchor') 184 | host = kargs.get('host') 185 | protocol = kargs.pop('protocol', None) 186 | qualified = kargs.pop('qualified', None) 187 | 188 | # Remove special words from kargs, convert placeholders 189 | for key in ['anchor', 'host']: 190 | if kargs.get(key): 191 | del kargs[key] 192 | if key+'_' in kargs: 193 | kargs[key] = kargs.pop(key+'_') 194 | 195 | if 'protocol_' in kargs: 196 | kargs['protocol_'] = protocol 197 | 198 | config = request_config() 199 | route = None 200 | static = False 201 | encoding = config.mapper.encoding 202 | url = '' 203 | if len(args) > 0: 204 | route = config.mapper._routenames.get(args[0]) 205 | 206 | # No named route found, assume the argument is a relative path 207 | if not route: 208 | static = True 209 | url = args[0] 210 | 211 | if url.startswith('/') and hasattr(config, 'environ') \ 212 | and config.environ.get('SCRIPT_NAME'): 213 | url = config.environ.get('SCRIPT_NAME') + url 214 | 215 | if static: 216 | if kargs: 217 | url += '?' 218 | query_args = [] 219 | for key, val in six.iteritems(kargs): 220 | if isinstance(val, (list, tuple)): 221 | for value in val: 222 | query_args.append("%s=%s" % ( 223 | urllib.parse.quote(six.text_type(key).encode(encoding)), 224 | urllib.parse.quote(six.text_type(value).encode(encoding)))) 225 | else: 226 | query_args.append("%s=%s" % ( 227 | urllib.parse.quote(six.text_type(key).encode(encoding)), 228 | urllib.parse.quote(six.text_type(val).encode(encoding)))) 229 | url += '&'.join(query_args) 230 | environ = getattr(config, 'environ', {}) 231 | if 'wsgiorg.routing_args' not in environ: 232 | environ = environ.copy() 233 | mapper_dict = getattr(config, 'mapper_dict', None) 234 | if mapper_dict is not None: 235 | match_dict = mapper_dict.copy() 236 | else: 237 | match_dict = {} 238 | environ['wsgiorg.routing_args'] = ((), match_dict) 239 | 240 | if not static: 241 | route_args = [] 242 | if route: 243 | if config.mapper.hardcode_names: 244 | route_args.append(route) 245 | newargs = route.defaults.copy() 246 | newargs.update(kargs) 247 | 248 | # If this route has a filter, apply it 249 | if route.filter: 250 | newargs = route.filter(newargs) 251 | 252 | if not route.static: 253 | # Handle sub-domains 254 | newargs = _subdomain_check(newargs, config.mapper, environ) 255 | else: 256 | newargs = _screenargs(kargs, config.mapper, environ) 257 | anchor = newargs.pop('_anchor', None) or anchor 258 | host = newargs.pop('_host', None) or host 259 | protocol = newargs.pop('_protocol', protocol) 260 | url = config.mapper.generate(*route_args, **newargs) 261 | if anchor is not None: 262 | url += '#' + _url_quote(anchor, encoding) 263 | if host or (protocol is not None) or qualified: 264 | if not host and not qualified: 265 | # Ensure we don't use a specific port, as changing the protocol 266 | # means that we most likely need a new port 267 | host = config.host.split(':')[0] 268 | elif not host: 269 | host = config.host 270 | if protocol is None: 271 | protocol = config.protocol 272 | if protocol != '': 273 | protocol += ':' 274 | if url is not None: 275 | url = protocol + '//' + host + url 276 | 277 | if not ascii_characters(url) and url is not None: 278 | raise GenerationException("url_for can only return a string, got " 279 | "unicode instead: %s" % url) 280 | if url is None: 281 | raise GenerationException( 282 | "url_for could not generate URL. Called with args: %s %s" % \ 283 | (args, kargs)) 284 | return url 285 | 286 | 287 | class URLGenerator(object): 288 | """The URL Generator generates URLs 289 | 290 | It is automatically instantiated by the RoutesMiddleware and put 291 | into the ``wsgiorg.routing_args`` tuple accessible as:: 292 | 293 | url = environ['wsgiorg.routing_args'][0][0] 294 | 295 | Or via the ``routes.url`` key:: 296 | 297 | url = environ['routes.url'] 298 | 299 | The url object may be instantiated outside of a web context for use 300 | in testing, however sub_domain support and fully qualified URLs 301 | cannot be generated without supplying a dict that must contain the 302 | key ``HTTP_HOST``. 303 | 304 | """ 305 | def __init__(self, mapper, environ): 306 | """Instantiate the URLGenerator 307 | 308 | ``mapper`` 309 | The mapper object to use when generating routes. 310 | ``environ`` 311 | The environment dict used in WSGI, alternately, any dict 312 | that contains at least an ``HTTP_HOST`` value. 313 | 314 | """ 315 | self.mapper = mapper 316 | if 'SCRIPT_NAME' not in environ: 317 | environ['SCRIPT_NAME'] = '' 318 | self.environ = environ 319 | 320 | def __call__(self, *args, **kargs): 321 | """Generates a URL 322 | 323 | All keys given to url_for are sent to the Routes Mapper instance for 324 | generation except for:: 325 | 326 | anchor specified the anchor name to be appened to the path 327 | host overrides the default (current) host if provided 328 | protocol overrides the default (current) protocol if provided 329 | qualified creates the URL with the host/port information as 330 | needed 331 | 332 | """ 333 | anchor = kargs.get('anchor') 334 | host = kargs.get('host') 335 | protocol = kargs.pop('protocol', None) 336 | qualified = kargs.pop('qualified', None) 337 | 338 | # Remove special words from kargs, convert placeholders 339 | for key in ['anchor', 'host']: 340 | if kargs.get(key): 341 | del kargs[key] 342 | if key+'_' in kargs: 343 | kargs[key] = kargs.pop(key+'_') 344 | 345 | if 'protocol_' in kargs: 346 | kargs['protocol_'] = protocol 347 | 348 | route = None 349 | use_current = '_use_current' in kargs and kargs.pop('_use_current') 350 | 351 | static = False 352 | encoding = self.mapper.encoding 353 | url = '' 354 | 355 | more_args = len(args) > 0 356 | if more_args: 357 | route = self.mapper._routenames.get(args[0]) 358 | 359 | if not route and more_args: 360 | static = True 361 | url = args[0] 362 | if url.startswith('/') and self.environ.get('SCRIPT_NAME'): 363 | url = self.environ.get('SCRIPT_NAME') + url 364 | 365 | if static: 366 | if kargs: 367 | url += '?' 368 | query_args = [] 369 | for key, val in six.iteritems(kargs): 370 | if isinstance(val, (list, tuple)): 371 | for value in val: 372 | query_args.append("%s=%s" % ( 373 | urllib.parse.quote(six.text_type(key).encode(encoding)), 374 | urllib.parse.quote(six.text_type(value).encode(encoding)))) 375 | else: 376 | query_args.append("%s=%s" % ( 377 | urllib.parse.quote(six.text_type(key).encode(encoding)), 378 | urllib.parse.quote(six.text_type(val).encode(encoding)))) 379 | url += '&'.join(query_args) 380 | if not static: 381 | route_args = [] 382 | if route: 383 | if self.mapper.hardcode_names: 384 | route_args.append(route) 385 | newargs = route.defaults.copy() 386 | newargs.update(kargs) 387 | 388 | # If this route has a filter, apply it 389 | if route.filter: 390 | newargs = route.filter(newargs) 391 | if not route.static or (route.static and not route.external): 392 | # Handle sub-domains, retain sub_domain if there is one 393 | sub = newargs.get('sub_domain', None) 394 | newargs = _subdomain_check(newargs, self.mapper, 395 | self.environ) 396 | # If the route requires a sub-domain, and we have it, restore 397 | # it 398 | if 'sub_domain' in route.defaults: 399 | newargs['sub_domain'] = sub 400 | 401 | elif use_current: 402 | newargs = _screenargs(kargs, self.mapper, self.environ, force_explicit=True) 403 | elif 'sub_domain' in kargs: 404 | newargs = _subdomain_check(kargs, self.mapper, self.environ) 405 | else: 406 | newargs = kargs 407 | 408 | anchor = anchor or newargs.pop('_anchor', None) 409 | host = host or newargs.pop('_host', None) 410 | if protocol is None: 411 | protocol = newargs.pop('_protocol', None) 412 | newargs['_environ'] = self.environ 413 | url = self.mapper.generate(*route_args, **newargs) 414 | if anchor is not None: 415 | url += '#' + _url_quote(anchor, encoding) 416 | if host or (protocol is not None) or qualified: 417 | if 'routes.cached_hostinfo' not in self.environ: 418 | cache_hostinfo(self.environ) 419 | hostinfo = self.environ['routes.cached_hostinfo'] 420 | 421 | if not host and not qualified: 422 | # Ensure we don't use a specific port, as changing the protocol 423 | # means that we most likely need a new port 424 | host = hostinfo['host'].split(':')[0] 425 | elif not host: 426 | host = hostinfo['host'] 427 | if protocol is None: 428 | protocol = hostinfo['protocol'] 429 | if protocol != '': 430 | protocol += ':' 431 | if url is not None: 432 | if host[-1] != '/': 433 | host += '/' 434 | url = protocol + '//' + host + url.lstrip('/') 435 | 436 | if not ascii_characters(url) and url is not None: 437 | raise GenerationException("Can only return a string, got " 438 | "unicode instead: %s" % url) 439 | if url is None: 440 | raise GenerationException( 441 | "Could not generate URL. Called with args: %s %s" % \ 442 | (args, kargs)) 443 | return url 444 | 445 | def current(self, *args, **kwargs): 446 | """Generate a route that includes params used on the current 447 | request 448 | 449 | The arguments for this method are identical to ``__call__`` 450 | except that arguments set to None will remove existing route 451 | matches of the same name from the set of arguments used to 452 | construct a URL. 453 | """ 454 | return self(_use_current=True, *args, **kwargs) 455 | 456 | 457 | def redirect_to(*args, **kargs): 458 | """Issues a redirect based on the arguments. 459 | 460 | Redirect's *should* occur as a "302 Moved" header, however the web 461 | framework may utilize a different method. 462 | 463 | All arguments are passed to url_for to retrieve the appropriate URL, then 464 | the resulting URL it sent to the redirect function as the URL. 465 | """ 466 | target = url_for(*args, **kargs) 467 | config = request_config() 468 | return config.redirect(target) 469 | 470 | 471 | def cache_hostinfo(environ): 472 | """Processes the host information and stores a copy 473 | 474 | This work was previously done but wasn't stored in environ, nor is 475 | it guaranteed to be setup in the future (Routes 2 and beyond). 476 | 477 | cache_hostinfo processes environ keys that may be present to 478 | determine the proper host, protocol, and port information to use 479 | when generating routes. 480 | 481 | """ 482 | hostinfo = {} 483 | if environ.get('HTTPS') or environ.get('wsgi.url_scheme') == 'https' \ 484 | or 'https' in environ.get('HTTP_X_FORWARDED_PROTO', "").split(', '): 485 | hostinfo['protocol'] = 'https' 486 | else: 487 | hostinfo['protocol'] = 'http' 488 | if environ.get('HTTP_X_FORWARDED_HOST'): 489 | hostinfo['host'] = environ['HTTP_X_FORWARDED_HOST'].split(', ', 1)[0] 490 | elif environ.get('HTTP_HOST'): 491 | hostinfo['host'] = environ['HTTP_HOST'] 492 | else: 493 | hostinfo['host'] = environ['SERVER_NAME'] 494 | if environ.get('wsgi.url_scheme') == 'https': 495 | if environ['SERVER_PORT'] != '443': 496 | hostinfo['host'] += ':' + environ['SERVER_PORT'] 497 | else: 498 | if environ['SERVER_PORT'] != '80': 499 | hostinfo['host'] += ':' + environ['SERVER_PORT'] 500 | environ['routes.cached_hostinfo'] = hostinfo 501 | return hostinfo 502 | 503 | 504 | def controller_scan(directory=None): 505 | """Scan a directory for python files and use them as controllers""" 506 | if directory is None: 507 | return [] 508 | 509 | def find_controllers(dirname, prefix=''): 510 | """Locate controllers in a directory""" 511 | controllers = [] 512 | for fname in os.listdir(dirname): 513 | filename = os.path.join(dirname, fname) 514 | if os.path.isfile(filename) and \ 515 | re.match(r'^[^_]{1,1}.*\.py$', fname): 516 | controllers.append(prefix + fname[:-3]) 517 | elif os.path.isdir(filename): 518 | controllers.extend(find_controllers(filename, 519 | prefix=prefix+fname+'/')) 520 | return controllers 521 | controllers = find_controllers(directory) 522 | # Sort by string length, shortest goes first 523 | controllers.sort(key=len, reverse=True) 524 | return controllers 525 | 526 | 527 | def as_unicode(value, encoding, errors='strict'): 528 | if value is not None and isinstance(value, bytes): 529 | return value.decode(encoding, errors) 530 | 531 | return value 532 | 533 | 534 | def ascii_characters(string): 535 | if string is None: 536 | return True 537 | 538 | return all(ord(c) < 128 for c in string) 539 | -------------------------------------------------------------------------------- /scripts/pylintrc: -------------------------------------------------------------------------------- 1 | # lint Python modules using external checkers. 2 | # 3 | # This is the main checker controling the other ones and the reports 4 | # generation. It is itself both a raw checker and an astng checker in order 5 | # to: 6 | # * handle message activation / deactivation at the module level 7 | # * handle some basic but necessary stats'data (number of classes, methods...) 8 | # 9 | [MASTER] 10 | 11 | # Specify a configuration file. 12 | #rcfile= 13 | 14 | # Profiled execution. 15 | profile=no 16 | 17 | # Add to the black list. It should be a base name, not a 18 | # path. You may set this option multiple times. 19 | ignore=.svn 20 | 21 | # Pickle collected data for later comparisons. 22 | persistent=yes 23 | 24 | # Set the cache size for astng objects. 25 | cache-size=500 26 | 27 | # List of plugins (as comma separated values of python modules names) to load, 28 | # usually to register additional checkers. 29 | load-plugins= 30 | 31 | 32 | [MESSAGES CONTROL] 33 | 34 | # Enable only checker(s) with the given id(s). This option conflict with the 35 | # disable-checker option 36 | #enable-checker= 37 | 38 | # Enable all checker(s) except those with the given id(s). This option conflict 39 | # with the disable-checker option 40 | #disable-checker= 41 | 42 | # Enable all messages in the listed categories. 43 | #enable-msg-cat= 44 | 45 | # Disable all messages in the listed categories. 46 | #disable-msg-cat= 47 | 48 | # Enable the message(s) with the given id(s). 49 | #enable-msg= 50 | 51 | # Disable the message(s) with the given id(s). 52 | disable-msg=C0323,W0142,C0301,C0103,C0111,E0213,C0302,C0203,W0703,R0201 53 | 54 | 55 | [REPORTS] 56 | 57 | # set the output format. Available formats are text, parseable, colorized and 58 | # html 59 | output-format=colorized 60 | 61 | # Include message's id in output 62 | include-ids=yes 63 | 64 | # Put messages in a separate file for each module / package specified on the 65 | # command line instead of printing them on stdout. Reports (if any) will be 66 | # written in a file name "pylint_global.[txt|html]". 67 | files-output=no 68 | 69 | # Tells wether to display a full report or only the messages 70 | reports=yes 71 | 72 | # Python expression which should return a note less than 10 (10 is the highest 73 | # note).You have access to the variables errors warning, statement which 74 | # respectivly contain the number of errors / warnings messages and the total 75 | # number of statements analyzed. This is used by the global evaluation report 76 | # (R0004). 77 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 78 | 79 | # Add a comment according to your evaluation note. This is used by the global 80 | # evaluation report (R0004). 81 | comment=no 82 | 83 | # Enable the report(s) with the given id(s). 84 | #enable-report= 85 | 86 | # Disable the report(s) with the given id(s). 87 | #disable-report= 88 | 89 | 90 | # checks for 91 | # * unused variables / imports 92 | # * undefined variables 93 | # * redefinition of variable from builtins or from an outer scope 94 | # * use of variable before assigment 95 | # 96 | [VARIABLES] 97 | 98 | # Tells wether we should check for unused import in __init__ files. 99 | init-import=no 100 | 101 | # A regular expression matching names used for dummy variables (i.e. not used). 102 | dummy-variables-rgx=_|dummy 103 | 104 | # List of additional names supposed to be defined in builtins. Remember that 105 | # you should avoid to define new builtins when possible. 106 | additional-builtins= 107 | 108 | 109 | # try to find bugs in the code using type inference 110 | # 111 | [TYPECHECK] 112 | 113 | # Tells wether missing members accessed in mixin class should be ignored. A 114 | # mixin class is detected if its name ends with "mixin" (case insensitive). 115 | ignore-mixin-members=yes 116 | 117 | # When zope mode is activated, consider the acquired-members option to ignore 118 | # access to some undefined attributes. 119 | zope=no 120 | 121 | # List of members which are usually get through zope's acquisition mecanism and 122 | # so shouldn't trigger E0201 when accessed (need zope=yes to be considered). 123 | acquired-members=REQUEST,acl_users,aq_parent 124 | 125 | 126 | # checks for : 127 | # * doc strings 128 | # * modules / classes / functions / methods / arguments / variables name 129 | # * number of arguments, local variables, branchs, returns and statements in 130 | # functions, methods 131 | # * required module attributes 132 | # * dangerous default values as arguments 133 | # * redefinition of function / method / class 134 | # * uses of the global statement 135 | # 136 | [BASIC] 137 | 138 | # Required attributes for module, separated by a comma 139 | required-attributes= 140 | 141 | # Regular expression which should only match functions or classes name which do 142 | # not require a docstring 143 | no-docstring-rgx=__.*__ 144 | 145 | # Regular expression which should only match correct module names 146 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 147 | 148 | # Regular expression which should only match correct module level names 149 | const-rgx=(([A-Z_][A-Z1-9_]*)|(__.*__))$ 150 | 151 | # Regular expression which should only match correct class names 152 | class-rgx=[A-Z_][a-zA-Z0-9]+$ 153 | 154 | # Regular expression which should only match correct function names 155 | function-rgx=[a-z_][a-z0-9_]{2,30}$ 156 | 157 | # Regular expression which should only match correct method names 158 | method-rgx=[a-z_][a-z0-9_]{2,30}$ 159 | 160 | # Regular expression which should only match correct instance attribute names 161 | attr-rgx=[a-z_][a-z0-9_]{2,30}$ 162 | 163 | # Regular expression which should only match correct argument names 164 | argument-rgx=[a-z_][a-z0-9_]{2,30}$ 165 | 166 | # Regular expression which should only match correct variable names 167 | variable-rgx=[a-z_][a-z0-9_]{2,30}$ 168 | 169 | # Regular expression which should only match correct list comprehension / 170 | # generator expression variable names 171 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 172 | 173 | # Good variable names which should always be accepted, separated by a comma 174 | good-names=i,j,k,ex,Run,_ 175 | 176 | # Bad variable names which should always be refused, separated by a comma 177 | bad-names=foo,bar,baz,toto,tutu,tata 178 | 179 | # List of builtins function names that should not be used, separated by a comma 180 | bad-functions=apply,input 181 | 182 | 183 | # checks for sign of poor/misdesign: 184 | # * number of methods, attributes, local variables... 185 | # * size, complexity of functions, methods 186 | # 187 | [DESIGN] 188 | 189 | # Maximum number of arguments for function / method 190 | max-args=12 191 | 192 | # Maximum number of locals for function / method body 193 | max-locals=30 194 | 195 | # Maximum number of return / yield for function / method body 196 | max-returns=12 197 | 198 | # Maximum number of branch for function / method body 199 | max-branchs=30 200 | 201 | # Maximum number of statements in function / method body 202 | max-statements=60 203 | 204 | # Maximum number of parents for a class (see R0901). 205 | max-parents=7 206 | 207 | # Maximum number of attributes for a class (see R0902). 208 | max-attributes=20 209 | 210 | # Minimum number of public methods for a class (see R0903). 211 | min-public-methods=0 212 | 213 | # Maximum number of public methods for a class (see R0904). 214 | max-public-methods=20 215 | 216 | 217 | # checks for 218 | # * external modules dependencies 219 | # * relative / wildcard imports 220 | # * cyclic imports 221 | # * uses of deprecated modules 222 | # 223 | [IMPORTS] 224 | 225 | # Deprecated modules which should not be used, separated by a comma 226 | deprecated-modules=regsub,string,TERMIOS,Bastion,rexec 227 | 228 | # Create a graph of every (i.e. internal and external) dependencies in the 229 | # given file (report R0402 must not be disabled) 230 | import-graph= 231 | 232 | # Create a graph of external dependencies in the given file (report R0402 must 233 | # not be disabled) 234 | ext-import-graph= 235 | 236 | # Create a graph of internal dependencies in the given file (report R0402 must 237 | # not be disabled) 238 | int-import-graph= 239 | 240 | 241 | # checks for : 242 | # * methods without self as first argument 243 | # * overridden methods signature 244 | # * access only to existant members via self 245 | # * attributes not defined in the __init__ method 246 | # * supported interfaces implementation 247 | # * unreachable code 248 | # 249 | [CLASSES] 250 | 251 | # List of interface methods to ignore, separated by a comma. This is used for 252 | # instance to not check methods defines in Zope's Interface base class. 253 | ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by 254 | 255 | # List of method names used to declare (i.e. assign) instance attributes. 256 | defining-attr-methods=__init__,__new__,setUp 257 | 258 | 259 | # checks for similarities and duplicated code. This computation may be 260 | # memory / CPU intensive, so you should disable it if you experiments some 261 | # problems. 262 | # 263 | [SIMILARITIES] 264 | 265 | # Minimum lines number of a similarity. 266 | min-similarity-lines=10 267 | 268 | # Ignore comments when computing similarities. 269 | ignore-comments=yes 270 | 271 | # Ignore docstrings when computing similarities. 272 | ignore-docstrings=yes 273 | 274 | 275 | # checks for: 276 | # * warning notes in the code like FIXME, XXX 277 | # * PEP 263: source code with non ascii character but no encoding declaration 278 | # 279 | [MISCELLANEOUS] 280 | 281 | # List of note tags to take in consideration, separated by a comma. 282 | notes=FIXME,XXX,TODO 283 | 284 | 285 | # checks for : 286 | # * unauthorized constructions 287 | # * strict indentation 288 | # * line length 289 | # * use of <> instead of != 290 | # 291 | [FORMAT] 292 | 293 | # Maximum number of characters on a single line. 294 | max-line-length=90 295 | 296 | # Maximum number of lines in a module 297 | max-module-lines=1000 298 | 299 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 300 | # tab). 301 | indent-string=' ' 302 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | clean_egg_info = egg_info -Db '' 3 | release = clean_egg_info sdist bdist_wheel 4 | 5 | [bdist_wheel] 6 | universal = 1 7 | 8 | [egg_info] 9 | tag_build = dev 10 | tag_date = 1 11 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | __version__ = '2.5.1' 2 | 3 | import io 4 | import os 5 | import sys 6 | 7 | from setuptools import setup, find_packages 8 | 9 | here = os.path.abspath(os.path.dirname(__file__)) 10 | with io.open(os.path.join(here, 'README.rst'), encoding='utf8') as f: 11 | README = f.read() 12 | with io.open(os.path.join(here, 'CHANGELOG.rst'), encoding='utf8') as f: 13 | CHANGES = f.read() 14 | PY3 = sys.version_info[0] == 3 15 | 16 | extra_options = { 17 | "packages": find_packages(), 18 | } 19 | 20 | extras_require = { 21 | 'middleware': [ 22 | 'webob', 23 | ] 24 | } 25 | extras_require['docs'] = ['Sphinx'] + extras_require['middleware'] 26 | 27 | if PY3: 28 | if "test" in sys.argv or "develop" in sys.argv: 29 | for root, directories, files in os.walk("tests"): 30 | for directory in directories: 31 | extra_options["packages"].append(os.path.join(root, directory)) 32 | 33 | setup(name="Routes", 34 | version=__version__, 35 | description='Routing Recognition and Generation Tools', 36 | long_description=README + '\n\n' + CHANGES, 37 | classifiers=["Development Status :: 5 - Production/Stable", 38 | "Intended Audience :: Developers", 39 | "License :: OSI Approved :: MIT License", 40 | "Topic :: Internet :: WWW/HTTP", 41 | "Topic :: Software Development :: Libraries :: Python Modules", 42 | "Programming Language :: Python :: Implementation :: PyPy", 43 | "Programming Language :: Python :: Implementation :: CPython", 44 | 'Programming Language :: Python', 45 | "Programming Language :: Python :: 2", 46 | "Programming Language :: Python :: 2.7", 47 | "Programming Language :: Python :: 3", 48 | "Programming Language :: Python :: 3.5", 49 | "Programming Language :: Python :: 3.6", 50 | "Programming Language :: Python :: 3.7", 51 | "Programming Language :: Python :: 3.8", 52 | "Programming Language :: Python :: 3.9" 53 | ], 54 | keywords='routes webob dispatch', 55 | author="Ben Bangert", 56 | author_email="ben@groovie.org", 57 | url='https://routes.readthedocs.io/', 58 | project_urls={ 59 | 'CI: GitHub': 'https://github.com/bbangert/routes/actions?query=branch:main', 60 | 'Docs: RTD': 'https://routes.readthedocs.io/', 61 | 'GitHub: issues': 'https://github.com/bbangert/routes/issues', 62 | 'GitHub: repo': 'https://github.com/bbangert/routes', 63 | }, 64 | license="MIT", 65 | include_package_data=True, 66 | zip_safe=False, 67 | install_requires=[ 68 | "six" 69 | ], 70 | extras_require=extras_require, 71 | **extra_options 72 | ) 73 | -------------------------------------------------------------------------------- /tests/test_files/controller_files/admin/users.py: -------------------------------------------------------------------------------- 1 | # 2 | -------------------------------------------------------------------------------- /tests/test_files/controller_files/content.py: -------------------------------------------------------------------------------- 1 | # 2 | -------------------------------------------------------------------------------- /tests/test_files/controller_files/users.py: -------------------------------------------------------------------------------- 1 | # 2 | -------------------------------------------------------------------------------- /tests/test_functional/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | -------------------------------------------------------------------------------- /tests/test_functional/profile_rec.py: -------------------------------------------------------------------------------- 1 | try: 2 | import profile 3 | import pstats 4 | except ImportError: 5 | pass 6 | import tempfile 7 | import os 8 | import time 9 | from routes import Mapper 10 | 11 | def get_mapper(): 12 | m = Mapper() 13 | m.connect('', controller='articles', action='index') 14 | m.connect('admin', controller='admin/general', action='index') 15 | 16 | m.connect('admin/comments/article/:article_id/:action/:id', 17 | controller = 'admin/comments', action = None, id=None) 18 | m.connect('admin/trackback/article/:article_id/:action/:id', 19 | controller='admin/trackback', action=None, id=None) 20 | m.connect('admin/content/:action/:id', controller='admin/content') 21 | 22 | m.connect('xml/:action/feed.xml', controller='xml') 23 | m.connect('xml/articlerss/:id/feed.xml', controller='xml', 24 | action='articlerss') 25 | m.connect('index.rdf', controller='xml', action='rss') 26 | 27 | m.connect('articles', controller='articles', action='index') 28 | m.connect('articles/page/:page', controller='articles', 29 | action='index', requirements = {'page':'\d+'}) 30 | 31 | m.connect( 32 | 'articles/:year/:month/:day/page/:page', 33 | controller='articles', action='find_by_date', month = None, 34 | day = None, 35 | requirements = {'year':'\d{4}', 'month':'\d{1,2}','day':'\d{1,2}'}) 36 | 37 | m.connect('articles/category/:id', controller='articles', action='category') 38 | m.connect('pages/*name', controller='articles', action='view_page') 39 | m.create_regs(['content','admin/why', 'admin/user']) 40 | return m 41 | 42 | def bench_rec(mapper, n): 43 | ts = time.time() 44 | for x in range(1,n): 45 | pass 46 | en = time.time() 47 | 48 | match = mapper.match 49 | 50 | # hits 51 | start = time.time() 52 | for x in range(1,n): 53 | match('/admin') 54 | match('/xml/1/feed.xml') 55 | match('/index.rdf') 56 | end = time.time() 57 | total = end-start-(en-ts) 58 | per_url = total / (n*10) 59 | print("Hit recognition\n") 60 | print("%s ms/url" % (per_url*1000)) 61 | print("%s urls/s\n" % (1.00/per_url)) 62 | 63 | # misses 64 | start = time.time() 65 | for x in range(1,n): 66 | match('/content') 67 | match('/content/list') 68 | match('/content/show/10') 69 | end = time.time() 70 | total = end-start-(en-ts) 71 | per_url = total / (n*10) 72 | print("Miss recognition\n") 73 | print("%s ms/url" % (per_url*1000)) 74 | print("%s urls/s\n" % (1.00/per_url)) 75 | 76 | def do_profile(cmd, globals, locals, sort_order, callers): 77 | fd, fn = tempfile.mkstemp() 78 | try: 79 | if hasattr(profile, 'runctx'): 80 | profile.runctx(cmd, globals, locals, fn) 81 | else: 82 | raise NotImplementedError( 83 | 'No profiling support under Python 2.3') 84 | stats = pstats.Stats(fn) 85 | stats.strip_dirs() 86 | # calls,time,cumulative and cumulative,calls,time are useful 87 | stats.sort_stats(*sort_order or ('cumulative', 'calls', 'time')) 88 | if callers: 89 | stats.print_callers() 90 | else: 91 | stats.print_stats() 92 | finally: 93 | os.remove(fn) 94 | 95 | def main(n=300): 96 | mapper = get_mapper() 97 | do_profile('bench_rec(mapper, %s)' % n, globals(), locals(), 98 | ('time', 'cumulative', 'calls'), None) 99 | 100 | if __name__ == '__main__': 101 | main() 102 | 103 | -------------------------------------------------------------------------------- /tests/test_functional/test_explicit_use.py: -------------------------------------------------------------------------------- 1 | """test_explicit_use""" 2 | import os, sys, time, unittest 3 | 4 | import pytest 5 | from routes import * 6 | from routes.route import Route 7 | from routes.util import GenerationException 8 | 9 | class TestUtils(unittest.TestCase): 10 | def test_route_dict_use(self): 11 | m = Mapper() 12 | m.explicit = True 13 | m.connect('/hi/{fred}') 14 | 15 | environ = {'HTTP_HOST': 'localhost'} 16 | 17 | env = environ.copy() 18 | env['PATH_INFO'] = '/hi/george' 19 | 20 | assert m.match(environ=env) == {'fred': 'george'} 21 | 22 | def test_x_forwarded(self): 23 | m = Mapper() 24 | m.explicit = True 25 | m.connect('/hi/{fred}') 26 | 27 | environ = {'HTTP_X_FORWARDED_HOST': 'localhost'} 28 | url = URLGenerator(m, environ) 29 | assert url(fred='smith', qualified=True) == 'http://localhost/hi/smith' 30 | 31 | def test_server_port(self): 32 | m = Mapper() 33 | m.explicit = True 34 | m.connect('/hi/{fred}') 35 | 36 | environ = {'SERVER_NAME': 'localhost', 'wsgi.url_scheme': 'https', 37 | 'SERVER_PORT': '993'} 38 | url = URLGenerator(m, environ) 39 | assert url(fred='smith', qualified=True) == 'https://localhost:993/hi/smith' 40 | 41 | def test_subdomain_screen(self): 42 | m = Mapper() 43 | m.explicit = True 44 | m.sub_domains = True 45 | m.connect('/hi/{fred}') 46 | 47 | environ = {'HTTP_HOST': 'localhost.com'} 48 | url = URLGenerator(m, environ) 49 | assert url(fred='smith', sub_domain=u'home', qualified=True) == 'http://home.localhost.com/hi/smith' 50 | 51 | environ = {'HTTP_HOST': 'here.localhost.com', 'PATH_INFO': '/hi/smith'} 52 | url = URLGenerator(m, environ.copy()) 53 | with pytest.raises(GenerationException): 54 | url.current(qualified=True) 55 | 56 | environ = {'HTTP_HOST': 'subdomain.localhost.com'} 57 | url = URLGenerator(m, environ.copy()) 58 | assert url(fred='smith', sub_domain='sub', qualified=True) == 'http://sub.localhost.com/hi/smith' 59 | 60 | environ = {'HTTP_HOST': 'sub.sub.localhost.com'} 61 | url = URLGenerator(m, environ.copy()) 62 | assert url(fred='smith', sub_domain='new', qualified=True) == 'http://new.localhost.com/hi/smith' 63 | 64 | url = URLGenerator(m, {}) 65 | assert url(fred='smith', sub_domain=u'home') == '/hi/smith' 66 | 67 | def test_anchor(self): 68 | m = Mapper() 69 | m.explicit = True 70 | m.connect('/hi/{fred}') 71 | 72 | environ = {'HTTP_HOST': 'localhost.com'} 73 | url = URLGenerator(m, environ) 74 | assert url(fred='smith', anchor='here') == '/hi/smith#here' 75 | 76 | def test_static_args(self): 77 | m = Mapper() 78 | m.explicit = True 79 | m.connect('http://google.com/', _static=True) 80 | 81 | url = URLGenerator(m, {}) 82 | 83 | assert url('/here', q=[u'fred', 'here now']) == '/here?q=fred&q=here%20now' 84 | 85 | def test_current(self): 86 | m = Mapper() 87 | m.explicit = True 88 | m.connect('/hi/{fred}') 89 | 90 | environ = {'HTTP_HOST': 'localhost.com', 'PATH_INFO': '/hi/smith'} 91 | match = m.routematch(environ=environ)[0] 92 | environ['wsgiorg.routing_args'] = (None, match) 93 | url = URLGenerator(m, environ) 94 | assert url.current() == '/hi/smith' 95 | 96 | def test_add_routes(self): 97 | map = Mapper(explicit=True) 98 | map.minimization = False 99 | routes = [ 100 | Route('foo', '/foo',) 101 | ] 102 | map.extend(routes) 103 | assert map.match('/foo') == {} 104 | 105 | def test_add_routes_conditions_unmet(self): 106 | map = Mapper(explicit=True) 107 | map.minimization = False 108 | routes = [ 109 | Route('foo', '/foo', conditions=dict(method=["POST"])) 110 | ] 111 | environ = { 112 | 'HTTP_HOST': 'localhost.com', 113 | 'PATH_INFO': '/foo', 114 | 'REQUEST_METHOD': 'GET', 115 | } 116 | map.extend(routes) 117 | assert map.match('/foo', environ=environ) is None 118 | 119 | def test_add_routes_conditions_met(self): 120 | map = Mapper(explicit=True) 121 | map.minimization = False 122 | routes = [ 123 | Route('foo', '/foo', conditions=dict(method=["POST"])) 124 | ] 125 | environ = { 126 | 'HTTP_HOST': 'localhost.com', 127 | 'PATH_INFO': '/foo', 128 | 'REQUEST_METHOD': 'POST', 129 | } 130 | map.extend(routes) 131 | assert map.match('/foo', environ=environ) == {} 132 | 133 | def test_using_func(self): 134 | def fred(view): 135 | pass 136 | 137 | m = Mapper() 138 | m.explicit = True 139 | m.connect('/hi/{fred}', controller=fred) 140 | 141 | environ = {'HTTP_HOST': 'localhost.com', 'PATH_INFO': '/hi/smith'} 142 | match = m.routematch(environ=environ)[0] 143 | environ['wsgiorg.routing_args'] = (None, match) 144 | url = URLGenerator(m, environ) 145 | assert url.current() == '/hi/smith' 146 | 147 | def test_using_prefix(self): 148 | m = Mapper() 149 | m.explicit = True 150 | m.connect('/{first}/{last}') 151 | 152 | environ = {'HTTP_HOST': 'localhost.com', 'PATH_INFO': '/content/index', 153 | 'SCRIPT_NAME': '/jones'} 154 | match = m.routematch(environ=environ)[0] 155 | environ['wsgiorg.routing_args'] = (None, match) 156 | url = URLGenerator(m, environ) 157 | 158 | assert url.current() == '/jones/content/index' 159 | assert url(first='smith', last='barney') == '/jones/smith/barney' 160 | 161 | def test_with_host_param(self): 162 | m = Mapper() 163 | m.explicit = True 164 | m.connect('/hi/{fred}') 165 | 166 | environ = {'HTTP_HOST': 'localhost.com'} 167 | url = URLGenerator(m, environ) 168 | assert url(fred='smith', host_='here') == '/hi/smith?host=here' 169 | -------------------------------------------------------------------------------- /tests/test_functional/test_middleware.py: -------------------------------------------------------------------------------- 1 | from routes import Mapper 2 | from routes.middleware import RoutesMiddleware 3 | from webtest import TestApp 4 | 5 | # Prevent pytest from trying to collect webtest's TestApp as tests: 6 | TestApp.__test__ = False 7 | 8 | 9 | def simple_app(environ, start_response): 10 | route_dict = environ['wsgiorg.routing_args'][1] 11 | start_response('200 OK', [('Content-type', 'text/plain')]) 12 | items = list(route_dict.items()) 13 | items.sort() 14 | return [('The matchdict items are %s and environ is %s' % (items, environ)).encode()] 15 | 16 | def test_basic(): 17 | map = Mapper(explicit=False) 18 | map.minimization = True 19 | map.connect(':controller/:action/:id') 20 | map.create_regs(['content']) 21 | app = TestApp(RoutesMiddleware(simple_app, map)) 22 | res = app.get('/') 23 | assert b'matchdict items are []' in res 24 | 25 | res = app.get('/content') 26 | assert b"matchdict items are [('action', 'index'), ('controller', " + repr( 27 | u'content').encode() + b"), ('id', None)]" in res 28 | 29 | def test_no_query(): 30 | map = Mapper(explicit=False) 31 | map.minimization = True 32 | map.connect('myapp/*path_info', controller='myapp') 33 | map.connect('project/*path_info', controller='myapp') 34 | map.create_regs(['content', 'myapp']) 35 | 36 | app = RoutesMiddleware(simple_app, map) 37 | env = {'PATH_INFO': '/', 'REQUEST_METHOD': 'GET', 'HTTP_HOST': 'localhost'} 38 | def start_response_wrapper(status, headers, exc=None): 39 | pass 40 | response = b''.join(app(env, start_response_wrapper)) 41 | assert b'matchdict items are []' in response 42 | 43 | def test_content_split(): 44 | map = Mapper(explicit=False) 45 | map.minimization = True 46 | map.connect('myapp/*path_info', controller='myapp') 47 | map.connect('project/*path_info', controller='myapp') 48 | map.create_regs(['content', 'myapp']) 49 | 50 | app = RoutesMiddleware(simple_app, map) 51 | env = {'PATH_INFO': '/', 'REQUEST_METHOD': 'POST', 'CONTENT_TYPE': 'text/plain;text/html', 52 | 'HTTP_HOST': 'localhost'} 53 | def start_response_wrapper(status, headers, exc=None): 54 | pass 55 | response = b''.join(app(env, start_response_wrapper)) 56 | assert b'matchdict items are []' in response 57 | 58 | def test_no_singleton(): 59 | map = Mapper(explicit=False) 60 | map.minimization = True 61 | map.connect('myapp/*path_info', controller='myapp') 62 | map.connect('project/*path_info', controller='myapp') 63 | map.create_regs(['content', 'myapp']) 64 | 65 | app = RoutesMiddleware(simple_app, map, singleton=False) 66 | env = {'PATH_INFO': '/', 'REQUEST_METHOD': 'POST', 'CONTENT_TYPE': 'text/plain;text/html'} 67 | def start_response_wrapper(status, headers, exc=None): 68 | pass 69 | response = b''.join(app(env, start_response_wrapper)) 70 | assert b'matchdict items are []' in response 71 | 72 | # Now a match 73 | env = {'PATH_INFO': '/project/fred', 'REQUEST_METHOD': 'POST', 'CONTENT_TYPE': 'text/plain;text/html'} 74 | def start_response_wrapper(status, headers, exc=None): 75 | pass 76 | response = b''.join(app(env, start_response_wrapper)) 77 | assert b"matchdict items are [('action', " + repr(u'index').encode() + \ 78 | b"), ('controller', " + repr(u'myapp').encode() + b"), ('path_info', 'fred')]" in response 79 | 80 | 81 | def test_path_info(): 82 | map = Mapper(explicit=False) 83 | map.minimization = True 84 | map.connect('myapp/*path_info', controller='myapp') 85 | map.connect('project/*path_info', controller='myapp') 86 | map.create_regs(['content', 'myapp']) 87 | 88 | app = TestApp(RoutesMiddleware(simple_app, map)) 89 | res = app.get('/') 90 | assert 'matchdict items are []' in res 91 | 92 | res = app.get('/myapp/some/other/url') 93 | print(res) 94 | assert b"matchdict items are [('action', " + repr(u'index').encode() + \ 95 | b"), ('controller', " + repr(u'myapp').encode() + b"), ('path_info', 'some/other/url')]" in res 96 | assert "'SCRIPT_NAME': '/myapp'" in res 97 | assert "'PATH_INFO': '/some/other/url'" in res 98 | 99 | res = app.get('/project/pylonshq/browser/pylons/templates/default_project/+package+/pylonshq/browser/pylons/templates/default_project/+package+/controllers') 100 | print(res) 101 | assert "'SCRIPT_NAME': '/project'" in res 102 | assert "'PATH_INFO': '/pylonshq/browser/pylons/templates/default_project/+package+/pylonshq/browser/pylons/templates/default_project/+package+/controllers'" in res 103 | 104 | def test_redirect_middleware(): 105 | map = Mapper(explicit=False) 106 | map.minimization = True 107 | map.connect('myapp/*path_info', controller='myapp') 108 | map.redirect("faq/{section}", "/static/faq/{section}.html") 109 | map.redirect("home/index", "/", _redirect_code='301 Moved Permanently') 110 | map.create_regs(['content', 'myapp']) 111 | 112 | app = TestApp(RoutesMiddleware(simple_app, map)) 113 | res = app.get('/') 114 | assert 'matchdict items are []' in res 115 | 116 | res = app.get('/faq/home') 117 | assert res.status == '302 Found' 118 | assert res.headers['Location'] == '/static/faq/home.html' 119 | 120 | res = app.get('/myapp/some/other/url') 121 | print(res) 122 | assert b"matchdict items are [('action', " + repr(u'index').encode() + \ 123 | b"), ('controller', " + repr(u'myapp').encode() + \ 124 | b"), ('path_info', 'some/other/url')]" in res 125 | assert "'SCRIPT_NAME': '/myapp'" in res 126 | assert "'PATH_INFO': '/some/other/url'" in res 127 | 128 | res = app.get('/home/index') 129 | assert '301 Moved Permanently' in res.status 130 | assert res.headers['Location'] == '/' 131 | 132 | def test_method_conversion(): 133 | map = Mapper(explicit=False) 134 | map.minimization = True 135 | map.connect('content/:type', conditions=dict(method='DELETE')) 136 | map.connect(':controller/:action/:id') 137 | map.create_regs(['content']) 138 | app = TestApp(RoutesMiddleware(simple_app, map)) 139 | res = app.get('/') 140 | assert 'matchdict items are []' in res 141 | 142 | res = app.get('/content') 143 | assert b"matchdict items are [('action', 'index'), ('controller', " + \ 144 | repr(u'content').encode() + b"), ('id', None)]" in res 145 | 146 | res = app.get('/content/hopper', params={'_method':'DELETE'}) 147 | assert b"matchdict items are [('action', " + repr(u'index').encode() + \ 148 | b"), ('controller', " + repr(u'content').encode() + \ 149 | b"), ('type', " + repr(u'hopper').encode() + b")]" in res 150 | 151 | res = app.post('/content/grind', 152 | params={'_method':'DELETE', 'name':'smoth'}, 153 | headers={'Content-Type': 'application/x-www-form-urlencoded'}) 154 | assert b"matchdict items are [('action', " + repr(u'index').encode() + \ 155 | b"), ('controller', " + repr(u'content').encode() + \ 156 | b"), ('type', " + repr(u'grind').encode() + b")]" in res 157 | assert "'REQUEST_METHOD': 'POST'" in res 158 | 159 | #res = app.post('/content/grind', 160 | # upload_files=[('fileupload', 'hello.txt', 'Hello World')], 161 | # params={'_method':'DELETE', 'name':'smoth'}) 162 | #assert "matchdict items are [('action', u'index'), ('controller', u'content'), ('type', u'grind')]" in res 163 | #assert "'REQUEST_METHOD': 'POST'" in res 164 | -------------------------------------------------------------------------------- /tests/test_functional/test_nonminimization.py: -------------------------------------------------------------------------------- 1 | """Test non-minimization recognition""" 2 | from six.moves import urllib 3 | 4 | from routes import url_for 5 | from routes.mapper import Mapper 6 | 7 | 8 | def test_basic(): 9 | m = Mapper(explicit=False) 10 | m.minimization = False 11 | m.connect('/:controller/:action/:id') 12 | m.create_regs(['content']) 13 | 14 | # Recognize 15 | assert m.match('/content') is None 16 | assert m.match('/content/index') is None 17 | assert m.match('/content/index/') is None 18 | assert m.match('/content/index/4') == {'controller':'content','action':'index','id':'4'} 19 | assert m.match('/content/view/4.html') == {'controller':'content','action':'view','id':'4.html'} 20 | 21 | # Generate 22 | assert m.generate(controller='content') is None 23 | assert m.generate(controller='content', id=4) == '/content/index/4' 24 | assert m.generate(controller='content', action='view', id=3) == '/content/view/3' 25 | 26 | def test_full(): 27 | m = Mapper(explicit=False) 28 | m.minimization = False 29 | m.connect('/:controller/:action/', id=None) 30 | m.connect('/:controller/:action/:id') 31 | m.create_regs(['content']) 32 | 33 | # Recognize 34 | assert m.match('/content') is None 35 | assert m.match('/content/index') is None 36 | assert m.match('/content/index/') == {'controller':'content','action':'index','id':None} 37 | assert m.match('/content/index/4') == {'controller':'content','action':'index','id':'4'} 38 | assert m.match('/content/view/4.html') == {'controller':'content','action':'view','id':'4.html'} 39 | 40 | # Generate 41 | assert m.generate(controller='content') is None 42 | 43 | # Looks odd, but only controller/action are set with non-explicit, so we 44 | # do need the id to match 45 | assert m.generate(controller='content', id=None) == '/content/index/' 46 | assert m.generate(controller='content', id=4) == '/content/index/4' 47 | assert m.generate(controller='content', action='view', id=3) == '/content/view/3' 48 | 49 | def test_action_required(): 50 | m = Mapper() 51 | m.minimization = False 52 | m.explicit = True 53 | m.connect('/:controller/index', action='index') 54 | m.create_regs(['content']) 55 | 56 | assert m.generate(controller='content') is None 57 | assert m.generate(controller='content', action='fred') is None 58 | assert m.generate(controller='content', action='index') == '/content/index' 59 | 60 | def test_query_params(): 61 | m = Mapper() 62 | m.minimization = False 63 | m.explicit = True 64 | m.connect('/:controller/index', action='index') 65 | m.create_regs(['content']) 66 | 67 | assert m.generate(controller='content') is None 68 | assert m.generate(controller='content', action='index', test='sample') == '/content/index?test=sample' 69 | 70 | 71 | def test_syntax(): 72 | m = Mapper(explicit=False) 73 | m.minimization = False 74 | m.connect('/{controller}/{action}/{id}') 75 | m.create_regs(['content']) 76 | 77 | # Recognize 78 | assert m.match('/content') is None 79 | assert m.match('/content/index') is None 80 | assert m.match('/content/index/') is None 81 | assert m.match('/content/index/4') == {'controller':'content','action':'index','id':'4'} 82 | 83 | # Generate 84 | assert m.generate(controller='content') is None 85 | assert m.generate(controller='content', id=4) == '/content/index/4' 86 | assert m.generate(controller='content', action='view', id=3) == '/content/view/3' 87 | 88 | def test_regexp_syntax(): 89 | m = Mapper(explicit=False) 90 | m.minimization = False 91 | m.connect('/{controller}/{action}/{id:\d\d}') 92 | m.create_regs(['content']) 93 | 94 | # Recognize 95 | assert m.match('/content') is None 96 | assert m.match('/content/index') is None 97 | assert m.match('/content/index/') is None 98 | assert m.match('/content/index/3') is None 99 | assert m.match('/content/index/44') == {'controller':'content','action':'index','id':'44'} 100 | 101 | # Generate 102 | assert m.generate(controller='content') is None 103 | assert m.generate(controller='content', id=4) is None 104 | assert m.generate(controller='content', id=43) == '/content/index/43' 105 | assert m.generate(controller='content', action='view', id=31) == '/content/view/31' 106 | 107 | def test_unicode(): 108 | hoge = u'\u30c6\u30b9\u30c8' # the word test in Japanese 109 | hoge_enc = urllib.parse.quote(hoge.encode('utf-8')) 110 | m = Mapper() 111 | m.minimization = False 112 | m.connect(':hoge') 113 | assert m.generate(hoge=hoge) == "/%s" % hoge_enc 114 | assert isinstance(m.generate(hoge=hoge), str) 115 | 116 | def test_unicode_static(): 117 | hoge = u'\u30c6\u30b9\u30c8' # the word test in Japanese 118 | hoge_enc = urllib.parse.quote(hoge.encode('utf-8')) 119 | m = Mapper() 120 | m.minimization = False 121 | m.connect('google-jp', 'http://www.google.co.jp/search', _static=True) 122 | m.create_regs(['messages']) 123 | assert url_for('google-jp', q=hoge) == "http://www.google.co.jp/search?q=" + hoge_enc 124 | assert isinstance(url_for('google-jp', q=hoge), str) 125 | 126 | def test_other_special_chars(): 127 | m = Mapper() 128 | m.minimization = False 129 | m.connect('/:year/:(slug).:(format),:(locale)', locale='en', format='html') 130 | m.create_regs(['content']) 131 | 132 | assert m.generate(year=2007, slug='test', format='xml', locale='ja') == '/2007/test.xml,ja' 133 | assert m.generate(year=2007, format='html') is None 134 | -------------------------------------------------------------------------------- /tests/test_functional/test_resources.py: -------------------------------------------------------------------------------- 1 | """test_resources""" 2 | import unittest 3 | import pytest 4 | 5 | from routes import * 6 | 7 | class TestResourceGeneration(unittest.TestCase): 8 | def _assert_restful_routes(self, m, options, path_prefix=''): 9 | baseroute = '/' + path_prefix + options['controller'] 10 | assert m.generate(action='index', **options) == baseroute 11 | assert m.generate(action='index', format='xml', **options) == baseroute + '.xml' 12 | assert m.generate(action='new', **options) == baseroute + '/new' 13 | assert m.generate(action='show', id='1', **options) == baseroute + '/1' 14 | assert m.generate(action='edit', id='1', **options) == baseroute + '/1/edit' 15 | assert m.generate(action='show', id='1', format='xml', **options) == baseroute + '/1.xml' 16 | 17 | assert m.generate(action='create', method='post', **options) == baseroute 18 | assert m.generate(action='update', method='put', id='1', **options) == baseroute + '/1' 19 | assert m.generate(action='delete', method='delete', id='1', **options) == baseroute + '/1' 20 | 21 | def test_resources(self): 22 | m = Mapper() 23 | m.resource('message', 'messages') 24 | m.resource('massage', 'massages') 25 | m.resource('passage', 'passages') 26 | m.create_regs(['messages']) 27 | options = dict(controller='messages') 28 | assert url_for('messages') == '/messages' 29 | assert url_for('formatted_messages', format='xml') == '/messages.xml' 30 | assert url_for('message', id=1) == '/messages/1' 31 | assert url_for('formatted_message', id=1, format='xml') == '/messages/1.xml' 32 | assert url_for('new_message') == '/messages/new' 33 | assert url_for('formatted_message', id=1, format='xml') == '/messages/1.xml' 34 | assert url_for('edit_message', id=1) == '/messages/1/edit' 35 | assert url_for('formatted_edit_message', id=1, format='xml') == '/messages/1/edit.xml' 36 | self._assert_restful_routes(m, options) 37 | 38 | def test_resources_with_path_prefix(self): 39 | m = Mapper() 40 | m.resource('message', 'messages', path_prefix='/thread/:threadid') 41 | m.create_regs(['messages']) 42 | options = dict(controller='messages', threadid='5') 43 | self._assert_restful_routes(m, options, path_prefix='thread/5/') 44 | 45 | def test_resources_with_collection_action(self): 46 | m = Mapper() 47 | m.resource('message', 'messages', collection=dict(rss='GET')) 48 | m.create_regs(['messages']) 49 | options = dict(controller='messages') 50 | self._assert_restful_routes(m, options) 51 | assert m.generate(controller='messages', action='rss') == '/messages/rss' 52 | assert url_for('rss_messages') == '/messages/rss' 53 | assert m.generate(controller='messages', action='rss', format='xml') == '/messages/rss.xml' 54 | assert url_for('formatted_rss_messages', format='xml') == '/messages/rss.xml' 55 | 56 | def test_resources_with_member_action(self): 57 | for method in ['put', 'post']: 58 | m = Mapper() 59 | m.resource('message', 'messages', member=dict(mark=method)) 60 | m.create_regs(['messages']) 61 | options = dict(controller='messages') 62 | self._assert_restful_routes(m, options) 63 | assert m.generate(method=method, action='mark', id='1', **options) == '/messages/1/mark' 64 | assert m.generate(method=method, action='mark', id='1', format='xml', **options) == '/messages/1/mark.xml' 65 | 66 | def test_resources_with_new_action(self): 67 | m = Mapper() 68 | m.resource('message', 'messages/', new=dict(preview='POST')) 69 | m.create_regs(['messages']) 70 | options = dict(controller='messages') 71 | self._assert_restful_routes(m, options) 72 | assert m.generate(controller='messages', action='preview', method='post') == '/messages/new/preview' 73 | assert url_for('preview_new_message') == '/messages/new/preview' 74 | assert m.generate(controller='messages', action='preview', method='post', format='xml') == '/messages/new/preview.xml' 75 | assert url_for('formatted_preview_new_message', format='xml') == '/messages/new/preview.xml' 76 | 77 | def test_resources_with_name_prefix(self): 78 | m = Mapper() 79 | m.resource('message', 'messages', name_prefix='category_', new=dict(preview='POST')) 80 | m.create_regs(['messages']) 81 | options = dict(controller='messages') 82 | self._assert_restful_routes(m, options) 83 | assert url_for('category_preview_new_message') == '/messages/new/preview' 84 | with pytest.raises(Exception): 85 | url_for('category_preview_new_message', method='get') 86 | 87 | def test_resources_with_requirements(self): 88 | m = Mapper() 89 | m.resource('message', 'messages', path_prefix='/{project_id}/{user_id}/', 90 | requirements={'project_id': r'[0-9a-f]{4}', 'user_id': r'\d+'}) 91 | options = dict(controller='messages', project_id='cafe', user_id='123') 92 | self._assert_restful_routes(m, options, path_prefix='cafe/123/') 93 | 94 | # in addition to the positive tests we need to guarantee we 95 | # are not matching when the requirements don't match. 96 | assert m.match('/cafe/123/messages') == {'action': u'create', 'project_id': u'cafe', 'user_id': u'123', 'controller': u'messages'} 97 | assert m.match('/extensions/123/messages') is None 98 | assert m.match('/b0a3/123b/messages') is None 99 | assert m.match('/foo/bar/messages') is None 100 | 101 | 102 | class TestResourceRecognition(unittest.TestCase): 103 | def test_resource(self): 104 | m = Mapper() 105 | m.resource('person', 'people') 106 | m.create_regs(['people']) 107 | 108 | con = request_config() 109 | con.mapper = m 110 | def test_path(path, method): 111 | env = dict(HTTP_HOST='example.com', PATH_INFO=path, REQUEST_METHOD=method) 112 | con.mapper_dict = {} 113 | con.environ = env 114 | 115 | test_path('/people', 'GET') 116 | assert con.mapper_dict == {'controller':'people', 'action':'index'} 117 | test_path('/people.xml', 'GET') 118 | assert con.mapper_dict == {'controller':'people', 'action':'index', 'format':'xml'} 119 | 120 | test_path('/people', 'POST') 121 | assert con.mapper_dict == {'controller':'people', 'action':'create'} 122 | test_path('/people.html', 'POST') 123 | assert con.mapper_dict == {'controller':'people', 'action':'create', 'format':'html'} 124 | 125 | test_path('/people/2.xml', 'GET') 126 | assert con.mapper_dict == {'controller':'people', 'action':'show', 'id':'2', 'format':'xml'} 127 | test_path('/people/2', 'GET') 128 | assert con.mapper_dict == {'controller':'people', 'action':'show', 'id':'2'} 129 | 130 | test_path('/people/2/edit', 'GET') 131 | assert con.mapper_dict == {'controller':'people', 'action':'edit', 'id':'2'} 132 | test_path('/people/2/edit.xml', 'GET') 133 | assert con.mapper_dict == {'controller':'people', 'action':'edit', 'id':'2', 'format':'xml'} 134 | 135 | test_path('/people/2', 'DELETE') 136 | assert con.mapper_dict == {'controller':'people', 'action':'delete', 'id':'2'} 137 | 138 | test_path('/people/2', 'PUT') 139 | assert con.mapper_dict == {'controller':'people', 'action':'update', 'id':'2'} 140 | test_path('/people/2.json', 'PUT') 141 | assert con.mapper_dict == {'controller':'people', 'action':'update', 'id':'2', 'format':'json'} 142 | 143 | # Test for dots in urls 144 | test_path('/people/2\.13', 'PUT') 145 | assert con.mapper_dict == {'controller':'people', 'action':'update', 'id':'2\.13'} 146 | test_path('/people/2\.13.xml', 'PUT') 147 | assert con.mapper_dict == {'controller':'people', 'action':'update', 'id':'2\.13', 'format':'xml'} 148 | test_path('/people/user\.name', 'PUT') 149 | assert con.mapper_dict == {'controller':'people', 'action':'update', 'id':'user\.name'} 150 | test_path('/people/user\.\.\.name', 'PUT') 151 | assert con.mapper_dict == {'controller':'people', 'action':'update', 'id':'user\.\.\.name'} 152 | test_path('/people/user\.name\.has\.dots', 'PUT') 153 | assert con.mapper_dict == {'controller':'people', 'action':'update', 'id':'user\.name\.has\.dots'} 154 | test_path('/people/user\.name\.is\.something.xml', 'PUT') 155 | assert con.mapper_dict == {'controller':'people', 'action':'update', 'id':'user\.name\.is\.something', 'format':'xml'} 156 | test_path('/people/user\.name\.ends\.with\.dot\..xml', 'PUT') 157 | assert con.mapper_dict == {'controller':'people', 'action':'update', 'id':'user\.name\.ends\.with\.dot\.', 'format':'xml'} 158 | test_path('/people/user\.name\.ends\.with\.dot\.', 'PUT') 159 | assert con.mapper_dict == {'controller':'people', 'action':'update', 'id':'user\.name\.ends\.with\.dot\.'} 160 | test_path('/people/\.user\.name\.starts\.with\.dot', 'PUT') 161 | assert con.mapper_dict == {'controller':'people', 'action':'update', 'id':'\.user\.name\.starts\.with\.dot'} 162 | test_path('/people/user\.name.json', 'PUT') 163 | assert con.mapper_dict == {'controller':'people', 'action':'update', 'id':'user\.name', 'format':'json'} 164 | 165 | def test_resource_with_nomin(self): 166 | m = Mapper() 167 | m.minimization = False 168 | m.resource('person', 'people') 169 | m.create_regs(['people']) 170 | 171 | con = request_config() 172 | con.mapper = m 173 | def test_path(path, method): 174 | env = dict(HTTP_HOST='example.com', PATH_INFO=path, REQUEST_METHOD=method) 175 | con.mapper_dict = {} 176 | con.environ = env 177 | 178 | test_path('/people', 'GET') 179 | assert con.mapper_dict == {'controller':'people', 'action':'index'} 180 | 181 | test_path('/people', 'POST') 182 | assert con.mapper_dict == {'controller':'people', 'action':'create'} 183 | 184 | test_path('/people/2', 'GET') 185 | assert con.mapper_dict == {'controller':'people', 'action':'show', 'id':'2'} 186 | test_path('/people/2/edit', 'GET') 187 | assert con.mapper_dict == {'controller':'people', 'action':'edit', 'id':'2'} 188 | 189 | test_path('/people/2', 'DELETE') 190 | assert con.mapper_dict == {'controller':'people', 'action':'delete', 'id':'2'} 191 | 192 | test_path('/people/2', 'PUT') 193 | assert con.mapper_dict == {'controller':'people', 'action':'update', 'id':'2'} 194 | 195 | def test_resource_created_with_parent_resource(self): 196 | m = Mapper() 197 | m.resource('location', 'locations', 198 | parent_resource=dict(member_name='region', 199 | collection_name='regions')) 200 | m.create_regs(['locations']) 201 | 202 | con = request_config() 203 | con.mapper = m 204 | def test_path(path, method): 205 | env = dict(HTTP_HOST='example.com', PATH_INFO=path, 206 | REQUEST_METHOD=method) 207 | con.mapper_dict = {} 208 | con.environ = env 209 | 210 | test_path('/regions/13/locations', 'GET') 211 | assert con.mapper_dict == {'region_id': '13', 'controller': 'locations', 'action': 'index'} 212 | url = url_for('region_locations', region_id=13) 213 | assert url == '/regions/13/locations' 214 | 215 | test_path('/regions/13/locations', 'POST') 216 | assert con.mapper_dict == {'region_id': '13', 'controller': 'locations', 'action': 'create'} 217 | # new 218 | url = url_for('region_new_location', region_id=13) 219 | assert url == '/regions/13/locations/new' 220 | # create 221 | url = url_for('region_locations', region_id=13) 222 | assert url == '/regions/13/locations' 223 | 224 | test_path('/regions/13/locations/60', 'GET') 225 | assert con.mapper_dict == {'region_id': '13', 'controller': 'locations', 'id': '60', 'action': 'show'} 226 | url = url_for('region_location', region_id=13, id=60) 227 | assert url == '/regions/13/locations/60' 228 | 229 | test_path('/regions/13/locations/60/edit', 'GET') 230 | assert con.mapper_dict == {'region_id': '13', 'controller': 'locations', 'id': '60', 'action': 'edit'} 231 | url = url_for('region_edit_location', region_id=13, id=60) 232 | assert url == '/regions/13/locations/60/edit' 233 | 234 | test_path('/regions/13/locations/60', 'DELETE') 235 | assert con.mapper_dict == {'region_id': '13', 'controller': 'locations', 'id': '60', 'action': 'delete'} 236 | url = url_for('region_location', region_id=13, id=60) 237 | assert url == '/regions/13/locations/60' 238 | 239 | test_path('/regions/13/locations/60', 'PUT') 240 | assert con.mapper_dict == {'region_id': '13', 'controller': 'locations', 'id': '60', 'action': 'update'} 241 | url = url_for('region_location', region_id=13, id=60) 242 | assert url == '/regions/13/locations/60' 243 | 244 | # Make sure ``path_prefix`` overrides work 245 | # empty ``path_prefix`` (though I'm not sure why someone would do this) 246 | m = Mapper() 247 | m.resource('location', 'locations', 248 | parent_resource=dict(member_name='region', 249 | collection_name='regions'), 250 | path_prefix='') 251 | url = url_for('region_locations') 252 | assert url == '/locations' 253 | # different ``path_prefix`` 254 | m = Mapper() 255 | m.resource('location', 'locations', 256 | parent_resource=dict(member_name='region', 257 | collection_name='regions'), 258 | path_prefix='areas/:area_id') 259 | url = url_for('region_locations', area_id=51) 260 | assert url == '/areas/51/locations' 261 | 262 | # Make sure ``name_prefix`` overrides work 263 | # empty ``name_prefix`` 264 | m = Mapper() 265 | m.resource('location', 'locations', 266 | parent_resource=dict(member_name='region', 267 | collection_name='regions'), 268 | name_prefix='') 269 | url = url_for('locations', region_id=51) 270 | assert url == '/regions/51/locations' 271 | # different ``name_prefix`` 272 | m = Mapper() 273 | m.resource('location', 'locations', 274 | parent_resource=dict(member_name='region', 275 | collection_name='regions'), 276 | name_prefix='area_') 277 | url = url_for('area_locations', region_id=51) 278 | assert url == '/regions/51/locations' 279 | 280 | # Make sure ``path_prefix`` and ``name_prefix`` overrides work together 281 | # empty ``path_prefix`` 282 | m = Mapper() 283 | m.resource('location', 'locations', 284 | parent_resource=dict(member_name='region', 285 | collection_name='regions'), 286 | path_prefix='', 287 | name_prefix='place_') 288 | url = url_for('place_locations') 289 | assert url == '/locations' 290 | # empty ``name_prefix`` 291 | m = Mapper() 292 | m.resource('location', 'locations', 293 | parent_resource=dict(member_name='region', 294 | collection_name='regions'), 295 | path_prefix='areas/:area_id', 296 | name_prefix='') 297 | url = url_for('locations', area_id=51) 298 | assert url == '/areas/51/locations' 299 | # different ``path_prefix`` and ``name_prefix`` 300 | m = Mapper() 301 | m.resource('location', 'locations', 302 | parent_resource=dict(member_name='region', 303 | collection_name='regions'), 304 | path_prefix='areas/:area_id', 305 | name_prefix='place_') 306 | url = url_for('place_locations', area_id=51) 307 | assert url == '/areas/51/locations' 308 | 309 | def test_resource_created_with_parent_resource_nomin(self): 310 | m = Mapper() 311 | m.minimization = False 312 | m.resource('location', 'locations', 313 | parent_resource=dict(member_name='region', 314 | collection_name='regions')) 315 | m.create_regs(['locations']) 316 | 317 | con = request_config() 318 | con.mapper = m 319 | def test_path(path, method): 320 | env = dict(HTTP_HOST='example.com', PATH_INFO=path, 321 | REQUEST_METHOD=method) 322 | con.mapper_dict = {} 323 | con.environ = env 324 | 325 | test_path('/regions/13/locations', 'GET') 326 | assert con.mapper_dict == {'region_id': '13', 'controller': 'locations', 'action': 'index'} 327 | url = url_for('region_locations', region_id=13) 328 | assert url == '/regions/13/locations' 329 | 330 | test_path('/regions/13/locations', 'POST') 331 | assert con.mapper_dict == {'region_id': '13', 'controller': 'locations', 'action': 'create'} 332 | # new 333 | url = url_for('region_new_location', region_id=13) 334 | assert url == '/regions/13/locations/new' 335 | # create 336 | url = url_for('region_locations', region_id=13) 337 | assert url == '/regions/13/locations' 338 | 339 | test_path('/regions/13/locations/60', 'GET') 340 | assert con.mapper_dict == {'region_id': '13', 'controller': 'locations', 'id': '60', 'action': 'show'} 341 | url = url_for('region_location', region_id=13, id=60) 342 | assert url == '/regions/13/locations/60' 343 | 344 | test_path('/regions/13/locations/60/edit', 'GET') 345 | assert con.mapper_dict == {'region_id': '13', 'controller': 'locations', 'id': '60', 'action': 'edit'} 346 | url = url_for('region_edit_location', region_id=13, id=60) 347 | assert url == '/regions/13/locations/60/edit' 348 | 349 | test_path('/regions/13/locations/60', 'DELETE') 350 | assert con.mapper_dict == {'region_id': '13', 'controller': 'locations', 'id': '60', 'action': 'delete'} 351 | url = url_for('region_location', region_id=13, id=60) 352 | assert url == '/regions/13/locations/60' 353 | 354 | test_path('/regions/13/locations/60', 'PUT') 355 | assert con.mapper_dict == {'region_id': '13', 'controller': 'locations', 'id': '60', 'action': 'update'} 356 | url = url_for('region_location', region_id=13, id=60) 357 | assert url == '/regions/13/locations/60' 358 | 359 | # Make sure ``path_prefix`` overrides work 360 | # empty ``path_prefix`` (though I'm not sure why someone would do this) 361 | m = Mapper() 362 | m.resource('location', 'locations', 363 | parent_resource=dict(member_name='region', 364 | collection_name='regions'), 365 | path_prefix='/') 366 | url = url_for('region_locations') 367 | assert url == '/locations' 368 | # different ``path_prefix`` 369 | m = Mapper() 370 | m.resource('location', 'locations', 371 | parent_resource=dict(member_name='region', 372 | collection_name='regions'), 373 | path_prefix='areas/:area_id') 374 | url = url_for('region_locations', area_id=51) 375 | assert url == '/areas/51/locations' 376 | 377 | # Make sure ``name_prefix`` overrides work 378 | # empty ``name_prefix`` 379 | m = Mapper() 380 | m.resource('location', 'locations', 381 | parent_resource=dict(member_name='region', 382 | collection_name='regions'), 383 | name_prefix='') 384 | url = url_for('locations', region_id=51) 385 | assert url == '/regions/51/locations' 386 | # different ``name_prefix`` 387 | m = Mapper() 388 | m.resource('location', 'locations', 389 | parent_resource=dict(member_name='region', 390 | collection_name='regions'), 391 | name_prefix='area_') 392 | url = url_for('area_locations', region_id=51) 393 | assert url == '/regions/51/locations' 394 | 395 | # Make sure ``path_prefix`` and ``name_prefix`` overrides work together 396 | # empty ``path_prefix`` 397 | m = Mapper() 398 | m.resource('location', 'locations', 399 | parent_resource=dict(member_name='region', 400 | collection_name='regions'), 401 | path_prefix='', 402 | name_prefix='place_') 403 | url = url_for('place_locations') 404 | assert url == '/locations' 405 | # empty ``name_prefix`` 406 | m = Mapper() 407 | m.resource('location', 'locations', 408 | parent_resource=dict(member_name='region', 409 | collection_name='regions'), 410 | path_prefix='areas/:area_id', 411 | name_prefix='') 412 | url = url_for('locations', area_id=51) 413 | assert url == '/areas/51/locations' 414 | # different ``path_prefix`` and ``name_prefix`` 415 | m = Mapper() 416 | m.resource('location', 'locations', 417 | parent_resource=dict(member_name='region', 418 | collection_name='regions'), 419 | path_prefix='areas/:area_id', 420 | name_prefix='place_') 421 | url = url_for('place_locations', area_id=51) 422 | assert url == '/areas/51/locations' 423 | 424 | 425 | 426 | if __name__ == '__main__': 427 | unittest.main() 428 | -------------------------------------------------------------------------------- /tests/test_functional/test_submapper.py: -------------------------------------------------------------------------------- 1 | """test_resources""" 2 | import unittest 3 | import pytest 4 | 5 | from routes import * 6 | 7 | class TestSubmapper(unittest.TestCase): 8 | def test_submapper(self): 9 | m = Mapper() 10 | c = m.submapper(path_prefix='/entries', requirements=dict(id='\d+')) 11 | c.connect('entry', '/{id}') 12 | 13 | assert url_for('entry', id=1) == '/entries/1' 14 | with pytest.raises(Exception): 15 | url_for('entry', id='foo') 16 | 17 | def test_submapper_with_no_path(self): 18 | m = Mapper() 19 | c = m.submapper(path_prefix='/') 20 | c.connect('entry') 21 | assert url_for('entry', id=1) == '/entry?id=1' 22 | 23 | def test_submapper_nesting(self): 24 | m = Mapper() 25 | c = m.submapper(path_prefix='/entries', controller='entry', 26 | requirements=dict(id='\d+')) 27 | e = c.submapper(path_prefix='/{id}') 28 | 29 | assert c.resource_name == 'entry' 30 | assert e.resource_name == 'entry' 31 | 32 | e.connect('entry', '') 33 | e.connect('edit_entry', '/edit') 34 | 35 | assert url_for('entry', id=1) == '/entries/1' 36 | assert url_for('edit_entry', id=1) == '/entries/1/edit' 37 | with pytest.raises(Exception): 38 | url_for('entry', id='foo') 39 | 40 | def test_submapper_action(self): 41 | m = Mapper(explicit=True) 42 | c = m.submapper(path_prefix='/entries', controller='entry') 43 | 44 | c.action(name='entries', action='list') 45 | c.action(action='create', method='POST') 46 | 47 | assert url_for('entries', method='GET') == '/entries' 48 | assert url_for('create_entry', method='POST') == '/entries' 49 | assert url_for(controller='entry', action='list', method='GET') == '/entries' 50 | assert url_for(controller='entry', action='create', method='POST') == '/entries' 51 | with pytest.raises(Exception): 52 | url_for('entries', method='DELETE') 53 | 54 | def test_submapper_link(self): 55 | m = Mapper(explicit=True) 56 | c = m.submapper(path_prefix='/entries', controller='entry') 57 | 58 | c.link(rel='new') 59 | c.link(rel='ping', method='POST') 60 | 61 | assert url_for('new_entry', method='GET') == '/entries/new' 62 | assert url_for('ping_entry', method='POST') == '/entries/ping' 63 | assert url_for(controller='entry', action='new', method='GET') == '/entries/new' 64 | assert url_for(controller='entry', action='ping', method='POST') == '/entries/ping' 65 | with pytest.raises(Exception): 66 | url_for('new_entry', method='PUT') 67 | with pytest.raises(Exception): 68 | url_for('ping_entry', method='PUT') 69 | 70 | def test_submapper_standard_actions(self): 71 | m = Mapper() 72 | c = m.submapper(path_prefix='/entries', collection_name='entries', 73 | controller='entry') 74 | e = c.submapper(path_prefix='/{id}') 75 | 76 | c.index() 77 | c.create() 78 | e.show() 79 | e.update() 80 | e.delete() 81 | 82 | assert url_for('entries', method='GET') == '/entries' 83 | assert url_for('create_entry', method='POST') == '/entries' 84 | with pytest.raises(Exception): 85 | url_for('entries', method='DELETE') 86 | 87 | assert url_for('entry', id=1, method='GET') == '/entries/1' 88 | assert url_for('update_entry', id=1, method='PUT') == '/entries/1' 89 | assert url_for('delete_entry', id=1, method='DELETE') == '/entries/1' 90 | with pytest.raises(Exception): 91 | url_for('entry', id=1, method='POST') 92 | 93 | def test_submapper_standard_links(self): 94 | m = Mapper() 95 | c = m.submapper(path_prefix='/entries', controller='entry') 96 | e = c.submapper(path_prefix='/{id}') 97 | 98 | c.new() 99 | e.edit() 100 | 101 | assert url_for('new_entry', method='GET') == '/entries/new' 102 | with pytest.raises(Exception): 103 | url_for('new_entry', method='POST') 104 | 105 | assert url_for('edit_entry', id=1, method='GET') == '/entries/1/edit' 106 | with pytest.raises(Exception): 107 | url_for('edit_entry', id=1, method='POST') 108 | 109 | def test_submapper_action_and_link_generation(self): 110 | m = Mapper() 111 | c = m.submapper(path_prefix='/entries', controller='entry', 112 | collection_name='entries', 113 | actions=['index', 'new', 'create']) 114 | e = c.submapper(path_prefix='/{id}', 115 | actions=['show', 'edit', 'update', 'delete']) 116 | 117 | assert url_for('entries', method='GET') == '/entries' 118 | assert url_for('create_entry', method='POST') == '/entries' 119 | with pytest.raises(Exception): 120 | url_for('entries', method='DELETE') 121 | 122 | assert url_for('entry', id=1, method='GET') == '/entries/1' 123 | assert url_for('update_entry', id=1, method='PUT') == '/entries/1' 124 | assert url_for('delete_entry', id=1, method='DELETE') == '/entries/1' 125 | with pytest.raises(Exception): 126 | url_for('entry', id=1, method='POST') 127 | 128 | assert url_for('new_entry', method='GET') == '/entries/new' 129 | with pytest.raises(Exception): 130 | url_for('new_entry', method='POST') 131 | 132 | assert url_for('edit_entry', id=1, method='GET') == '/entries/1/edit' 133 | with pytest.raises(Exception): 134 | url_for('edit_entry', id=1, method='POST') 135 | 136 | def test_collection(self): 137 | m = Mapper() 138 | c = m.collection('entries', 'entry') 139 | 140 | assert url_for('entries', method='GET') == '/entries' 141 | assert url_for('create_entry', method='POST') == '/entries' 142 | with pytest.raises(Exception): 143 | url_for('entries', method='DELETE') 144 | 145 | assert url_for('entry', id=1, method='GET') == '/entries/1' 146 | assert url_for('update_entry', id=1, method='PUT') == '/entries/1' 147 | assert url_for('delete_entry', id=1, method='DELETE') == '/entries/1' 148 | with pytest.raises(Exception): 149 | url_for('entry', id=1, method='POST') 150 | 151 | assert url_for('new_entry', method='GET') == '/entries/new' 152 | with pytest.raises(Exception): 153 | url_for('new_entry', method='POST') 154 | 155 | assert url_for('edit_entry', id=1, method='GET') == '/entries/1/edit' 156 | with pytest.raises(Exception): 157 | url_for('edit_entry', id=1, method='POST') 158 | 159 | def test_collection_options(self): 160 | m = Mapper() 161 | requirement=dict(id='\d+') 162 | c = m.collection('entries', 'entry', conditions=dict(sub_domain=True), 163 | requirements=requirement) 164 | for r in m.matchlist: 165 | assert r.conditions['sub_domain'] is True 166 | assert r.reqs == requirement 167 | 168 | def test_subsubmapper_with_controller(self): 169 | m = Mapper() 170 | col1 = m.collection('parents', 'parent', 171 | controller='col1', 172 | member_prefix='/{parent_id}') 173 | # NOTE: If one uses functions as controllers, the error will be here. 174 | col2 = col1.member.collection('children', 'child', 175 | controller='col2', 176 | member_prefix='/{child_id}') 177 | match = m.match('/parents/1/children/2') 178 | assert match.get('controller') == 'col2' 179 | 180 | def test_submapper_argument_overriding(self): 181 | m = Mapper() 182 | first = m.submapper(path_prefix='/first_level', 183 | controller='first', action='test', 184 | name_prefix='first_') 185 | first.connect('test', r'/test') 186 | second = first.submapper(path_prefix='/second_level', 187 | controller='second', 188 | name_prefix='second_') 189 | second.connect('test', r'/test') 190 | third = second.submapper(path_prefix='/third_level', 191 | controller="third", action='third_action', 192 | name_prefix='third_') 193 | third.connect('test', r'/test') 194 | 195 | # test first level 196 | match = m.match('/first_level/test') 197 | assert match.get('controller') == 'first' 198 | assert match.get('action') == 'test' 199 | # test name_prefix worked 200 | assert url_for('first_test') == '/first_level/test' 201 | 202 | # test second level controller override 203 | match = m.match('/first_level/second_level/test') 204 | assert match.get('controller') == 'second' 205 | assert match.get('action') == 'test' 206 | # test name_prefix worked 207 | assert url_for('first_second_test') == '/first_level/second_level/test' 208 | 209 | # test third level controller and action override 210 | match = m.match('/first_level/second_level/third_level/test') 211 | assert match.get('controller') == 'third' 212 | assert match.get('action') == 'third_action' 213 | # test name_prefix worked 214 | assert url_for('first_second_third_test') == '/first_level/second_level/third_level/test' 215 | 216 | 217 | if __name__ == '__main__': 218 | unittest.main() 219 | -------------------------------------------------------------------------------- /tests/test_units/test_base.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from routes import request_config, _RequestConfig 3 | from routes.base import Route 4 | 5 | class TestBase(unittest.TestCase): 6 | def test_route(self): 7 | route = Route(None, ':controller/:action/:id') 8 | assert not route.static 9 | 10 | def test_request_config(self): 11 | orig_config = request_config() 12 | class Obby(object): pass 13 | myobj = Obby() 14 | class MyCallable(object): 15 | def __init__(self): 16 | class Obby(object): pass 17 | self.obj = myobj 18 | 19 | def __call__(self): 20 | return self.obj 21 | 22 | mycall = MyCallable() 23 | if hasattr(orig_config, 'using_request_local'): 24 | orig_config.request_local = mycall 25 | config = request_config() 26 | assert id(myobj) == id(config) 27 | old_config = request_config(original=True) 28 | assert issubclass(old_config.__class__, _RequestConfig) is True 29 | del orig_config.request_local 30 | 31 | if __name__ == '__main__': 32 | unittest.main() 33 | -------------------------------------------------------------------------------- /tests/test_units/test_environment.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import routes 3 | 4 | class TestEnvironment(unittest.TestCase): 5 | def setUp(self): 6 | m = routes.Mapper(explicit=False) 7 | m.minimization = True 8 | m.connect('archive/:year/:month/:day', controller='blog', action='view', month=None, day=None, 9 | requirements={'month':'\d{1,2}','day':'\d{1,2}'}) 10 | m.connect('viewpost/:id', controller='post', action='view') 11 | m.connect(':controller/:action/:id') 12 | m.create_regs(['content', 'blog']) 13 | con = routes.request_config() 14 | con.mapper = m 15 | self.con = con 16 | 17 | def test_env_set(self): 18 | env = dict(PATH_INFO='/content', HTTP_HOST='somewhere.com') 19 | con = self.con 20 | con.mapper_dict = {} 21 | assert con.mapper_dict == {} 22 | delattr(con, 'mapper_dict') 23 | 24 | assert not hasattr(con, 'mapper_dict') 25 | con.mapper_dict = {} 26 | 27 | con.environ = env 28 | assert con.mapper.environ == env 29 | assert con.protocol == 'http' 30 | assert con.host == 'somewhere.com' 31 | assert 'controller' in con.mapper_dict 32 | assert con.mapper_dict['controller'] == 'content' 33 | 34 | if __name__ == '__main__': 35 | unittest.main() 36 | -------------------------------------------------------------------------------- /tests/test_units/test_mapper_str.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from routes import Mapper 3 | 4 | class TestMapperStr(unittest.TestCase): 5 | def test_str(self): 6 | m = Mapper() 7 | m.connect('/{controller}/{action}') 8 | m.connect('entries', '/entries', controller='entry', action='index') 9 | m.connect('entry', '/entries/{id}', controller='entry', action='show') 10 | 11 | expected = """\ 12 | Route name Methods Path Controller action 13 | /{controller}/{action} 14 | entries /entries entry index 15 | entry /entries/{id} entry show""" 16 | 17 | for expected_line, actual_line in zip(expected.splitlines(), str(m).splitlines()): 18 | assert expected_line == actual_line.rstrip() 19 | -------------------------------------------------------------------------------- /tests/test_units/test_route_escapes.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from routes.route import Route 3 | 4 | 5 | class TestRouteEscape(unittest.TestCase): 6 | def test_normal_route(self): 7 | r = Route('test', '/foo/bar') 8 | self.assertEqual(r.routelist, ['/foo/bar']) 9 | 10 | def test_route_with_backslash(self): 11 | r = Route('test', '/foo\\\\bar') 12 | self.assertEqual(r.routelist, ['/foo\\bar']) 13 | 14 | def test_route_with_random_escapes(self): 15 | r = Route('test', '\\/f\\oo\\/ba\\r') 16 | self.assertEqual(r.routelist, ['\\/f\\oo\\/ba\\r']) 17 | 18 | def test_route_with_colon(self): 19 | r = Route('test', '/foo:bar/baz') 20 | self.assertEqual( 21 | r.routelist, ['/foo', {'name': 'bar', 'type': ':'}, '/', 'baz']) 22 | 23 | def test_route_with_escaped_colon(self): 24 | r = Route('test', '/foo\\:bar/baz') 25 | self.assertEqual(r.routelist, ['/foo:bar/baz']) 26 | 27 | def test_route_with_both_colons(self): 28 | r = Route('test', '/prefix/escaped\\:escaped/foo=:notescaped/bar=42') 29 | self.assertEqual( 30 | r.routelist, ['/prefix/escaped:escaped/foo=', 31 | {'name': 'notescaped', 'type': ':'}, '/', 'bar=42']) 32 | 33 | def test_route_with_all_escapes(self): 34 | r = Route('test', '/hmm\\:\\*\\{\\}*star/{brackets}/:colon') 35 | self.assertEqual( 36 | r.routelist, ['/hmm:*{}', {'name': 'star', 'type': '*'}, '/', 37 | {'name': 'brackets', 'type': ':'}, '/', 38 | {'name': 'colon', 'type': ':'}]) 39 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py{27,35,36},pypy,pypy3,style 3 | 4 | [testenv] 5 | deps= 6 | pytest 7 | pytest-cov 8 | soupsieve<2.0 9 | webtest 10 | commands= 11 | py.test --cov=routes --cov-report html:html_coverage -v {posargs:tests/} 12 | 13 | # We could to this in dependencies, but explicit is better than implicit 14 | pip install .[middleware] 15 | # webob optional dependency is fulfilled by [middleware] extra requirement 16 | python -c "import webob" 17 | 18 | [testenv:style] 19 | deps = flake8 20 | commands = flake8 routes 21 | 22 | [flake8] 23 | # These are all ignored until someone decided to go fix them 24 | ignore = E125,E127,E128,E226,E305,E402,E501,E502,E504,F401,W503,W504 25 | --------------------------------------------------------------------------------