From cd5ab51eca5ca95c31eaa12192234938bc1435cb Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 7 Nov 2013 01:08:39 -0600 Subject: support query string and anchor on external static urls --- pyramid/config/views.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 69f68e422..3b768b0e9 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -36,6 +36,8 @@ from pyramid.interfaces import ( from pyramid import renderers from pyramid.compat import ( + native_, + text_type, string_types, urlparse, url_quote, @@ -44,6 +46,8 @@ from pyramid.compat import ( is_nonstr_iter ) +from pyramid.encode import urlencode + from pyramid.exceptions import ( ConfigurationError, PredicateMismatch, @@ -1902,7 +1906,18 @@ class StaticURLInfo(object): url = urlparse.urlunparse(url_parse( url, scheme=request.environ['wsgi.url_scheme'])) subpath = url_quote(subpath) - return urljoin(url, subpath) + result = urljoin(url, subpath) + if '_query' in kw: + query = kw.pop('_query') + if isinstance(query, text_type): + result += '?' + native_(query) + elif query: + result += '?' + urlencode(query, doseq=True) + if '_anchor' in kw: + anchor = kw.pop('_anchor') + anchor = native_(anchor, 'utf-8') + result += '#' + anchor + return result raise ValueError('No static URL definition matching %s' % path) -- cgit v1.2.3 From 86946fde64505c55424eed4cf891b8c4a98cefd3 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 7 Nov 2013 01:09:41 -0600 Subject: support encoding arbitrary query strings --- pyramid/url.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pyramid/url.py b/pyramid/url.py index fda2c72c7..2e3542161 100644 --- a/pyramid/url.py +++ b/pyramid/url.py @@ -222,7 +222,9 @@ class URLMethodsMixin(object): if '_query' in kw: query = kw.pop('_query') - if query: + if isinstance(query, text_type): + qs = '?' + native_(query) + elif query: qs = '?' + urlencode(query, doseq=True) if '_anchor' in kw: @@ -580,7 +582,9 @@ class URLMethodsMixin(object): if 'query' in kw: query = kw['query'] - if query: + if isinstance(query, text_type): + qs = '?' + native_(query) + elif query: qs = '?' + urlencode(query, doseq=True) if 'anchor' in kw: -- cgit v1.2.3 From e967a9da69291d603a395550b0f30868a25c4ea5 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 7 Nov 2013 01:29:10 -0600 Subject: tests for custom query strings --- pyramid/encode.py | 13 +++++++------ pyramid/tests/test_url.py | 17 +++++++++++++++++ pyramid/url.py | 14 +++++++++----- 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/pyramid/encode.py b/pyramid/encode.py index 9e190bc21..d2376109e 100644 --- a/pyramid/encode.py +++ b/pyramid/encode.py @@ -3,7 +3,7 @@ from pyramid.compat import ( binary_type, is_nonstr_iter, url_quote as _url_quote, - url_quote_plus as quote_plus, # bw compat api (dnr) + url_quote_plus as _quote_plus, ) def url_quote(s, safe=''): # bw compat api @@ -47,28 +47,29 @@ def urlencode(query, doseq=True): prefix = '' for (k, v) in query: - k = _enc(k) + k = quote_plus(k) if is_nonstr_iter(v): for x in v: - x = _enc(x) + x = quote_plus(x) result += '%s%s=%s' % (prefix, k, x) prefix = '&' elif v is None: result += '%s%s=' % (prefix, k) else: - v = _enc(v) + v = quote_plus(v) result += '%s%s=%s' % (prefix, k, v) prefix = '&' return result -def _enc(val): +# bw compat api (dnr) +def quote_plus(val): cls = val.__class__ if cls is text_type: val = val.encode('utf-8') elif cls is not binary_type: val = str(val).encode('utf-8') - return quote_plus(val) + return _quote_plus(val) diff --git a/pyramid/tests/test_url.py b/pyramid/tests/test_url.py index f6117777f..9cee7f61c 100644 --- a/pyramid/tests/test_url.py +++ b/pyramid/tests/test_url.py @@ -93,6 +93,14 @@ class TestURLMethodsMixin(unittest.TestCase): result = request.resource_url(context, 'a b c') self.assertEqual(result, 'http://example.com:5432/context/a%20b%20c') + def test_resource_url_with_query_str(self): + request = self._makeOne() + self._registerResourceURL(request.registry) + context = DummyContext() + result = request.resource_url(context, 'a', query='(openlayers)') + self.assertEqual(result, + 'http://example.com:5432/context/a?%28openlayers%29') + def test_resource_url_with_query_dict(self): request = self._makeOne() self._registerResourceURL(request.registry) @@ -483,6 +491,15 @@ class TestURLMethodsMixin(unittest.TestCase): self.assertEqual(result, 'http://example.com:5432/1/2/3?q=1') + def test_route_url_with_query_str(self): + from pyramid.interfaces import IRoutesMapper + request = self._makeOne() + mapper = DummyRoutesMapper(route=DummyRoute('/1/2/3')) + request.registry.registerUtility(mapper, IRoutesMapper) + result = request.route_url('flub', _query='(openlayers)') + self.assertEqual(result, + 'http://example.com:5432/1/2/3?%28openlayers%29') + def test_route_url_with_empty_query(self): from pyramid.interfaces import IRoutesMapper request = self._makeOne() diff --git a/pyramid/url.py b/pyramid/url.py index 2e3542161..4803283c3 100644 --- a/pyramid/url.py +++ b/pyramid/url.py @@ -14,10 +14,14 @@ from pyramid.interfaces import ( from pyramid.compat import ( native_, bytes_, + string_types, text_type, url_quote, ) -from pyramid.encode import urlencode +from pyramid.encode import ( + quote_plus, + urlencode, +) from pyramid.path import caller_package from pyramid.threadlocal import get_current_registry @@ -222,8 +226,8 @@ class URLMethodsMixin(object): if '_query' in kw: query = kw.pop('_query') - if isinstance(query, text_type): - qs = '?' + native_(query) + if isinstance(query, string_types): + qs = '?' + quote_plus(query) elif query: qs = '?' + urlencode(query, doseq=True) @@ -582,8 +586,8 @@ class URLMethodsMixin(object): if 'query' in kw: query = kw['query'] - if isinstance(query, text_type): - qs = '?' + native_(query) + if isinstance(query, string_types): + qs = '?' + quote_plus(query) elif query: qs = '?' + urlencode(query, doseq=True) -- cgit v1.2.3 From fc30e8e2bac083907f0fcda6fddfd30d0d48751f Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 7 Nov 2013 01:54:50 -0600 Subject: fix mishandled anchors that need to be quoted --- pyramid/config/views.py | 7 +++++-- pyramid/tests/test_config/test_views.py | 11 +++++++++++ pyramid/tests/test_url.py | 33 ++++++++------------------------- pyramid/url.py | 4 ++-- 4 files changed, 26 insertions(+), 29 deletions(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 3b768b0e9..190eefc98 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -46,7 +46,10 @@ from pyramid.compat import ( is_nonstr_iter ) -from pyramid.encode import urlencode +from pyramid.encode import ( + quote_plus, + urlencode, +) from pyramid.exceptions import ( ConfigurationError, @@ -1915,7 +1918,7 @@ class StaticURLInfo(object): result += '?' + urlencode(query, doseq=True) if '_anchor' in kw: anchor = kw.pop('_anchor') - anchor = native_(anchor, 'utf-8') + anchor = quote_plus(anchor) result += '#' + anchor return result diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index 051961d25..a7e32f0c6 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -3820,6 +3820,17 @@ class TestStaticURLInfo(unittest.TestCase): result = inst.generate('package:path/abc def', request, a=1) self.assertEqual(result, 'http://example.com/abc%20def') + def test_generate_url_with_custom_anchor(self): + inst = self._makeOne() + registrations = [('http://example.com/', 'package:path/', None)] + inst._get_registrations = lambda *x: registrations + request = self._makeRequest() + uc = text_(b'La Pe\xc3\xb1a', 'utf-8') + result = inst.generate('package:path/abc def', request, a=1, + _anchor=uc) + self.assertEqual(result, + 'http://example.com/abc%20def#La+Pe%C3%B1a') + def test_add_already_exists(self): inst = self._makeOne() config = self._makeConfig( diff --git a/pyramid/tests/test_url.py b/pyramid/tests/test_url.py index 9cee7f61c..80dbd7001 100644 --- a/pyramid/tests/test_url.py +++ b/pyramid/tests/test_url.py @@ -157,15 +157,10 @@ class TestURLMethodsMixin(unittest.TestCase): request = self._makeOne() self._registerResourceURL(request.registry) context = DummyContext() - uc = text_(b'La Pe\xc3\xb1a', 'utf-8') + uc = text_(b'La Pe\xc3\xb1a', 'utf-8') result = request.resource_url(context, anchor=uc) - self.assertEqual( - result, - native_( - text_(b'http://example.com:5432/context/#La Pe\xc3\xb1a', - 'utf-8'), - 'utf-8') - ) + self.assertEqual(result, + 'http://example.com:5432/context/#La+Pe%C3%B1a') def test_resource_url_anchor_is_not_urlencoded(self): request = self._makeOne() @@ -173,7 +168,7 @@ class TestURLMethodsMixin(unittest.TestCase): context = DummyContext() result = request.resource_url(context, anchor=' /#') self.assertEqual(result, - 'http://example.com:5432/context/# /#') + 'http://example.com:5432/context/#+%2F%23') def test_resource_url_no_IResourceURL_registered(self): # falls back to ResourceURL @@ -456,14 +451,8 @@ class TestURLMethodsMixin(unittest.TestCase): request.registry.registerUtility(mapper, IRoutesMapper) result = request.route_url('flub', _anchor=b"La Pe\xc3\xb1a") - self.assertEqual( - result, - native_( - text_( - b'http://example.com:5432/1/2/3#La Pe\xc3\xb1a', - 'utf-8'), - 'utf-8') - ) + self.assertEqual(result, + 'http://example.com:5432/1/2/3#La+Pe%C3%B1a') def test_route_url_with_anchor_unicode(self): from pyramid.interfaces import IRoutesMapper @@ -473,14 +462,8 @@ class TestURLMethodsMixin(unittest.TestCase): anchor = text_(b'La Pe\xc3\xb1a', 'utf-8') result = request.route_url('flub', _anchor=anchor) - self.assertEqual( - result, - native_( - text_( - b'http://example.com:5432/1/2/3#La Pe\xc3\xb1a', - 'utf-8'), - 'utf-8') - ) + self.assertEqual(result, + 'http://example.com:5432/1/2/3#La+Pe%C3%B1a') def test_route_url_with_query(self): from pyramid.interfaces import IRoutesMapper diff --git a/pyramid/url.py b/pyramid/url.py index 4803283c3..eb9b364d9 100644 --- a/pyramid/url.py +++ b/pyramid/url.py @@ -233,7 +233,7 @@ class URLMethodsMixin(object): if '_anchor' in kw: anchor = kw.pop('_anchor') - anchor = native_(anchor, 'utf-8') + anchor = quote_plus(anchor) anchor = '#' + anchor if '_app_url' in kw: @@ -594,7 +594,7 @@ class URLMethodsMixin(object): if 'anchor' in kw: anchor = kw['anchor'] if isinstance(anchor, text_type): - anchor = native_(anchor, 'utf-8') + anchor = quote_plus(anchor) anchor = '#' + anchor if elements: -- cgit v1.2.3 From 8f3bf659488c1a946af2cb74c7a1fd4179c350de Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 7 Nov 2013 01:55:07 -0600 Subject: handle static urls generated with a query string --- pyramid/config/views.py | 2 +- pyramid/tests/test_config/test_views.py | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 190eefc98..022984420 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1913,7 +1913,7 @@ class StaticURLInfo(object): if '_query' in kw: query = kw.pop('_query') if isinstance(query, text_type): - result += '?' + native_(query) + result += '?' + quote_plus(query) elif query: result += '?' + urlencode(query, doseq=True) if '_anchor' in kw: diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index a7e32f0c6..832921713 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -3820,6 +3820,16 @@ class TestStaticURLInfo(unittest.TestCase): result = inst.generate('package:path/abc def', request, a=1) self.assertEqual(result, 'http://example.com/abc%20def') + def test_generate_url_with_custom_query(self): + inst = self._makeOne() + registrations = [('http://example.com/', 'package:path/', None)] + inst._get_registrations = lambda *x: registrations + request = self._makeRequest() + result = inst.generate('package:path/abc def', request, a=1, + _query='(openlayers)') + self.assertEqual(result, + 'http://example.com/abc%20def?%28openlayers%29') + def test_generate_url_with_custom_anchor(self): inst = self._makeOne() registrations = [('http://example.com/', 'package:path/', None)] -- cgit v1.2.3 From 70381b1ac56348c025261f1e7300e0b63891d363 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 7 Nov 2013 19:24:20 -0600 Subject: fix tests --- pyramid/config/views.py | 4 +--- pyramid/tests/test_url.py | 2 +- pyramid/url.py | 5 +---- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 022984420..0a47d869a 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -36,8 +36,6 @@ from pyramid.interfaces import ( from pyramid import renderers from pyramid.compat import ( - native_, - text_type, string_types, urlparse, url_quote, @@ -1912,7 +1910,7 @@ class StaticURLInfo(object): result = urljoin(url, subpath) if '_query' in kw: query = kw.pop('_query') - if isinstance(query, text_type): + if isinstance(query, string_types): result += '?' + quote_plus(query) elif query: result += '?' + urlencode(query, doseq=True) diff --git a/pyramid/tests/test_url.py b/pyramid/tests/test_url.py index 80dbd7001..cbbb933a7 100644 --- a/pyramid/tests/test_url.py +++ b/pyramid/tests/test_url.py @@ -162,7 +162,7 @@ class TestURLMethodsMixin(unittest.TestCase): self.assertEqual(result, 'http://example.com:5432/context/#La+Pe%C3%B1a') - def test_resource_url_anchor_is_not_urlencoded(self): + def test_resource_url_anchor_is_urlencoded(self): request = self._makeOne() self._registerResourceURL(request.registry) context = DummyContext() diff --git a/pyramid/url.py b/pyramid/url.py index eb9b364d9..4384e9cd6 100644 --- a/pyramid/url.py +++ b/pyramid/url.py @@ -12,10 +12,8 @@ from pyramid.interfaces import ( ) from pyramid.compat import ( - native_, bytes_, string_types, - text_type, url_quote, ) from pyramid.encode import ( @@ -593,8 +591,7 @@ class URLMethodsMixin(object): if 'anchor' in kw: anchor = kw['anchor'] - if isinstance(anchor, text_type): - anchor = quote_plus(anchor) + anchor = quote_plus(anchor) anchor = '#' + anchor if elements: -- cgit v1.2.3 From 093127f6f0492343521d6a07475178c0efb0fd98 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 12 Nov 2013 00:24:08 -0600 Subject: remove need for parsing static url twice to replace scheme --- pyramid/config/views.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 0a47d869a..5ad235795 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1902,10 +1902,8 @@ class StaticURLInfo(object): else: parsed = url_parse(url) if not parsed.scheme: - # parsed.scheme is readonly, so we have to parse again - # to change the scheme, sigh. - url = urlparse.urlunparse(url_parse( - url, scheme=request.environ['wsgi.url_scheme'])) + url = urlparse.urlunparse(parsed._replace( + scheme=request.environ['wsgi.url_scheme'])) subpath = url_quote(subpath) result = urljoin(url, subpath) if '_query' in kw: -- cgit v1.2.3 From af3134a984a7e9c53d41607dcf4f1feb60282f85 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 12 Nov 2013 00:56:57 -0600 Subject: centralize and properly escape query string and anchor arguments --- pyramid/config/views.py | 16 +++---- pyramid/encode.py | 4 +- pyramid/tests/test_config/test_views.py | 2 +- pyramid/tests/test_url.py | 10 ++--- pyramid/url.py | 77 +++++++++++++++++++-------------- 5 files changed, 58 insertions(+), 51 deletions(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 5ad235795..0165f96f1 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -70,6 +70,8 @@ from pyramid.security import NO_PERMISSION_REQUIRED from pyramid.static import static_view from pyramid.threadlocal import get_current_registry +from pyramid.url import parse_url_overrides + from pyramid.view import ( render_view_to_response, AppendSlashNotFoundViewFactory, @@ -1900,23 +1902,15 @@ class StaticURLInfo(object): kw['subpath'] = subpath return request.route_url(route_name, **kw) else: + app_url, scheme, host, port, qs, anchor = \ + parse_url_overrides(kw) parsed = url_parse(url) if not parsed.scheme: url = urlparse.urlunparse(parsed._replace( scheme=request.environ['wsgi.url_scheme'])) subpath = url_quote(subpath) result = urljoin(url, subpath) - if '_query' in kw: - query = kw.pop('_query') - if isinstance(query, string_types): - result += '?' + quote_plus(query) - elif query: - result += '?' + urlencode(query, doseq=True) - if '_anchor' in kw: - anchor = kw.pop('_anchor') - anchor = quote_plus(anchor) - result += '#' + anchor - return result + return result + qs + anchor raise ValueError('No static URL definition matching %s' % path) diff --git a/pyramid/encode.py b/pyramid/encode.py index d2376109e..15da1c511 100644 --- a/pyramid/encode.py +++ b/pyramid/encode.py @@ -65,11 +65,11 @@ def urlencode(query, doseq=True): return result # bw compat api (dnr) -def quote_plus(val): +def quote_plus(val, safe=''): cls = val.__class__ if cls is text_type: val = val.encode('utf-8') elif cls is not binary_type: val = str(val).encode('utf-8') - return _quote_plus(val) + return _quote_plus(val, safe=safe) diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index 832921713..c722c9166 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -3828,7 +3828,7 @@ class TestStaticURLInfo(unittest.TestCase): result = inst.generate('package:path/abc def', request, a=1, _query='(openlayers)') self.assertEqual(result, - 'http://example.com/abc%20def?%28openlayers%29') + 'http://example.com/abc%20def?(openlayers)') def test_generate_url_with_custom_anchor(self): inst = self._makeOne() diff --git a/pyramid/tests/test_url.py b/pyramid/tests/test_url.py index cbbb933a7..6a8624a9f 100644 --- a/pyramid/tests/test_url.py +++ b/pyramid/tests/test_url.py @@ -99,7 +99,7 @@ class TestURLMethodsMixin(unittest.TestCase): context = DummyContext() result = request.resource_url(context, 'a', query='(openlayers)') self.assertEqual(result, - 'http://example.com:5432/context/a?%28openlayers%29') + 'http://example.com:5432/context/a?(openlayers)') def test_resource_url_with_query_dict(self): request = self._makeOne() @@ -162,13 +162,13 @@ class TestURLMethodsMixin(unittest.TestCase): self.assertEqual(result, 'http://example.com:5432/context/#La+Pe%C3%B1a') - def test_resource_url_anchor_is_urlencoded(self): + def test_resource_url_anchor_is_urlencoded_safe(self): request = self._makeOne() self._registerResourceURL(request.registry) context = DummyContext() - result = request.resource_url(context, anchor=' /#') + result = request.resource_url(context, anchor=' /#?&+') self.assertEqual(result, - 'http://example.com:5432/context/#+%2F%23') + 'http://example.com:5432/context/#+/%23?&+') def test_resource_url_no_IResourceURL_registered(self): # falls back to ResourceURL @@ -481,7 +481,7 @@ class TestURLMethodsMixin(unittest.TestCase): request.registry.registerUtility(mapper, IRoutesMapper) result = request.route_url('flub', _query='(openlayers)') self.assertEqual(result, - 'http://example.com:5432/1/2/3?%28openlayers%29') + 'http://example.com:5432/1/2/3?(openlayers)') def test_route_url_with_empty_query(self): from pyramid.interfaces import IRoutesMapper diff --git a/pyramid/url.py b/pyramid/url.py index 4384e9cd6..e760bb356 100644 --- a/pyramid/url.py +++ b/pyramid/url.py @@ -29,6 +29,48 @@ from pyramid.traversal import ( ) PATH_SAFE = '/:@&+$,' # from webob +QUERY_SAFE = '/?:@!$&\'()*+,;=' # RFC 3986 +ANCHOR_SAFE = QUERY_SAFE + +def parse_url_overrides(kw): + """Parse special arguments passed when generating urls. + + The supplied dictionary is mutated, popping arguments as necessary. + Returns a 6-tuple of the format ``(app_url, scheme, host, port, + qs, anchor)``. + """ + anchor = '' + qs = '' + app_url = None + host = None + scheme = None + port = None + + if '_query' in kw: + query = kw.pop('_query') + if isinstance(query, string_types): + qs = '?' + quote_plus(query, safe=QUERY_SAFE) + elif query: + qs = '?' + urlencode(query, doseq=True) + + if '_anchor' in kw: + anchor = kw.pop('_anchor') + anchor = quote_plus(anchor, safe=ANCHOR_SAFE) + anchor = '#' + anchor + + if '_app_url' in kw: + app_url = kw.pop('_app_url') + + if '_host' in kw: + host = kw.pop('_host') + + if '_scheme' in kw: + scheme = kw.pop('_scheme') + + if '_port' in kw: + port = kw.pop('_port') + + return app_url, scheme, host, port, qs, anchor class URLMethodsMixin(object): """ Request methods mixin for BaseRequest having to do with URL @@ -215,36 +257,7 @@ class URLMethodsMixin(object): if route.pregenerator is not None: elements, kw = route.pregenerator(self, elements, kw) - anchor = '' - qs = '' - app_url = None - host = None - scheme = None - port = None - - if '_query' in kw: - query = kw.pop('_query') - if isinstance(query, string_types): - qs = '?' + quote_plus(query) - elif query: - qs = '?' + urlencode(query, doseq=True) - - if '_anchor' in kw: - anchor = kw.pop('_anchor') - anchor = quote_plus(anchor) - anchor = '#' + anchor - - if '_app_url' in kw: - app_url = kw.pop('_app_url') - - if '_host' in kw: - host = kw.pop('_host') - - if '_scheme' in kw: - scheme = kw.pop('_scheme') - - if '_port' in kw: - port = kw.pop('_port') + app_url, scheme, host, port, qs, anchor = parse_url_overrides(kw) if app_url is None: if (scheme is not None or host is not None or port is not None): @@ -585,13 +598,13 @@ class URLMethodsMixin(object): if 'query' in kw: query = kw['query'] if isinstance(query, string_types): - qs = '?' + quote_plus(query) + qs = '?' + quote_plus(query, safe=QUERY_SAFE) elif query: qs = '?' + urlencode(query, doseq=True) if 'anchor' in kw: anchor = kw['anchor'] - anchor = quote_plus(anchor) + anchor = quote_plus(anchor, safe=ANCHOR_SAFE) anchor = '#' + anchor if elements: -- cgit v1.2.3 From 22f0ebbc04f1fa03139ca7c99e02e39a2635590f Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 12 Nov 2013 01:13:48 -0600 Subject: modify quoting to be bare-bones --- pyramid/encode.py | 10 +++++++--- pyramid/tests/test_config/test_views.py | 2 +- pyramid/tests/test_url.py | 8 ++++---- pyramid/url.py | 11 +++++------ 4 files changed, 17 insertions(+), 14 deletions(-) diff --git a/pyramid/encode.py b/pyramid/encode.py index 15da1c511..0be0107b3 100644 --- a/pyramid/encode.py +++ b/pyramid/encode.py @@ -6,8 +6,13 @@ from pyramid.compat import ( url_quote_plus as _quote_plus, ) -def url_quote(s, safe=''): # bw compat api - return _url_quote(s, safe=safe) +def url_quote(val, safe=''): # bw compat api + cls = val.__class__ + if cls is text_type: + val = val.encode('utf-8') + elif cls is not binary_type: + val = str(val).encode('utf-8') + return _url_quote(val, safe=safe) def urlencode(query, doseq=True): """ @@ -72,4 +77,3 @@ def quote_plus(val, safe=''): elif cls is not binary_type: val = str(val).encode('utf-8') return _quote_plus(val, safe=safe) - diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index c722c9166..57bb5e9d0 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -3839,7 +3839,7 @@ class TestStaticURLInfo(unittest.TestCase): result = inst.generate('package:path/abc def', request, a=1, _anchor=uc) self.assertEqual(result, - 'http://example.com/abc%20def#La+Pe%C3%B1a') + 'http://example.com/abc%20def#La%20Pe%C3%B1a') def test_add_already_exists(self): inst = self._makeOne() diff --git a/pyramid/tests/test_url.py b/pyramid/tests/test_url.py index 6a8624a9f..22ccd1d0e 100644 --- a/pyramid/tests/test_url.py +++ b/pyramid/tests/test_url.py @@ -160,7 +160,7 @@ class TestURLMethodsMixin(unittest.TestCase): uc = text_(b'La Pe\xc3\xb1a', 'utf-8') result = request.resource_url(context, anchor=uc) self.assertEqual(result, - 'http://example.com:5432/context/#La+Pe%C3%B1a') + 'http://example.com:5432/context/#La%20Pe%C3%B1a') def test_resource_url_anchor_is_urlencoded_safe(self): request = self._makeOne() @@ -168,7 +168,7 @@ class TestURLMethodsMixin(unittest.TestCase): context = DummyContext() result = request.resource_url(context, anchor=' /#?&+') self.assertEqual(result, - 'http://example.com:5432/context/#+/%23?&+') + 'http://example.com:5432/context/#%20/%23?&+') def test_resource_url_no_IResourceURL_registered(self): # falls back to ResourceURL @@ -452,7 +452,7 @@ class TestURLMethodsMixin(unittest.TestCase): result = request.route_url('flub', _anchor=b"La Pe\xc3\xb1a") self.assertEqual(result, - 'http://example.com:5432/1/2/3#La+Pe%C3%B1a') + 'http://example.com:5432/1/2/3#La%20Pe%C3%B1a') def test_route_url_with_anchor_unicode(self): from pyramid.interfaces import IRoutesMapper @@ -463,7 +463,7 @@ class TestURLMethodsMixin(unittest.TestCase): result = request.route_url('flub', _anchor=anchor) self.assertEqual(result, - 'http://example.com:5432/1/2/3#La+Pe%C3%B1a') + 'http://example.com:5432/1/2/3#La%20Pe%C3%B1a') def test_route_url_with_query(self): from pyramid.interfaces import IRoutesMapper diff --git a/pyramid/url.py b/pyramid/url.py index e760bb356..629c1531a 100644 --- a/pyramid/url.py +++ b/pyramid/url.py @@ -14,10 +14,9 @@ from pyramid.interfaces import ( from pyramid.compat import ( bytes_, string_types, - url_quote, ) from pyramid.encode import ( - quote_plus, + url_quote, urlencode, ) from pyramid.path import caller_package @@ -49,13 +48,13 @@ def parse_url_overrides(kw): if '_query' in kw: query = kw.pop('_query') if isinstance(query, string_types): - qs = '?' + quote_plus(query, safe=QUERY_SAFE) + qs = '?' + url_quote(query, QUERY_SAFE) elif query: qs = '?' + urlencode(query, doseq=True) if '_anchor' in kw: anchor = kw.pop('_anchor') - anchor = quote_plus(anchor, safe=ANCHOR_SAFE) + anchor = url_quote(anchor, ANCHOR_SAFE) anchor = '#' + anchor if '_app_url' in kw: @@ -598,13 +597,13 @@ class URLMethodsMixin(object): if 'query' in kw: query = kw['query'] if isinstance(query, string_types): - qs = '?' + quote_plus(query, safe=QUERY_SAFE) + qs = '?' + url_quote(query, QUERY_SAFE) elif query: qs = '?' + urlencode(query, doseq=True) if 'anchor' in kw: anchor = kw['anchor'] - anchor = quote_plus(anchor, safe=ANCHOR_SAFE) + anchor = url_quote(anchor, ANCHOR_SAFE) anchor = '#' + anchor if elements: -- cgit v1.2.3 From a3654e4866303695478fb7bd01dcdd602a717b4e Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 12 Nov 2013 01:36:10 -0600 Subject: update docs --- CHANGES.txt | 11 +++++++++++ pyramid/url.py | 45 ++++++++++++++++++++++++++++++++++----------- 2 files changed, 45 insertions(+), 11 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 895dc572f..bf1c1ea01 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -29,6 +29,17 @@ Features timeouts, and conformance with the ``ISession`` API. See https://github.com/Pylons/pyramid/pull/1142 +- Allow ``pyramid.request.Request.route_url`` and + ``pyramid.request.Request.resource_url`` to accept strings for their + query string to enable alternative encodings. Also the anchor argument + will now be escaped to ensure minimal conformance. + See https://github.com/Pylons/pyramid/pull/1183 + +- Allow sending of ``_query`` and ``_anchor`` options to + ``pyramid.request.Request.static_url`` when an external URL is being + generated. + See https://github.com/Pylons/pyramid/pull/1183 + Bug Fixes --------- diff --git a/pyramid/url.py b/pyramid/url.py index 629c1531a..14f4add35 100644 --- a/pyramid/url.py +++ b/pyramid/url.py @@ -171,11 +171,15 @@ class URLMethodsMixin(object): query string will be returned in the URL. If it is present, it will be used to compose a query string that will be tacked on to the end of the URL, replacing any request query string. - The value of ``_query`` must be a sequence of two-tuples *or* + The value of ``_query`` may be a sequence of two-tuples *or* a data structure with an ``.items()`` method that returns a sequence of two-tuples (presumably a dictionary). This data structure will be turned into a query string per the - documentation of :func:`pyramid.encode.urlencode` function. + documentation of :func:`pyramid.url.urlencode` function. + Alternative encodings may be used by passing a string for ``_query`` + in which case it will be quoted as per :rfc:`3986#section-3.4` but + no other assumptions will be made about the data format. For example, + spaces will be escaped as ``%20`` instead of ``+``. After the query data is turned into a query string, a leading ``?`` is prepended, and the resulting string is appended to the generated URL. @@ -189,8 +193,13 @@ class URLMethodsMixin(object): as values, and a k=v pair will be placed into the query string for each value. + .. versionchanged:: 1.5 + Allow the ``_query`` option to be a string to enable alternative + encodings. + If a keyword argument ``_anchor`` is present, its string - representation will be used as a named anchor in the generated URL + representation will be quoted per :rfc:`3986#section-3.5` and used as + a named anchor in the generated URL (e.g. if ``_anchor`` is passed as ``foo`` and the route URL is ``http://example.com/route/url``, the resulting generated URL will be ``http://example.com/route/url#foo``). @@ -199,8 +208,11 @@ class URLMethodsMixin(object): If ``_anchor`` is passed as a string, it should be UTF-8 encoded. If ``_anchor`` is passed as a Unicode object, it will be converted to - UTF-8 before being appended to the URL. The anchor value is not - quoted in any way before being appended to the generated URL. + UTF-8 before being appended to the URL. + + .. versionchanged:: 1.5 + The ``_anchor`` option will be escaped instead of using + its raw string representation. If both ``_anchor`` and ``_query`` are specified, the anchor element will always follow the query element, @@ -351,13 +363,17 @@ class URLMethodsMixin(object): If a keyword argument ``query`` is present, it will be used to compose a query string that will be tacked on to the end of the URL. - The value of ``query`` must be a sequence of two-tuples *or* a data + The value of ``query`` may be a sequence of two-tuples *or* a data structure with an ``.items()`` method that returns a sequence of two-tuples (presumably a dictionary). This data structure will be turned into a query string per the documentation of - ``pyramid.url.urlencode`` function. After the query data is turned - into a query string, a leading ``?`` is prepended, and the resulting - string is appended to the generated URL. + :func:``pyramid.url.urlencode`` function. + Alternative encodings may be used by passing a string for ``query`` + in which case it will be quoted as per :rfc:`3986#section-3.4` but + no other assumptions will be made about the data format. For example, + spaces will be escaped as ``%20`` instead of ``+``. + After the query data is turned into a query string, a leading ``?`` is + prepended, and the resulting string is appended to the generated URL. .. note:: @@ -368,6 +384,10 @@ class URLMethodsMixin(object): as values, and a k=v pair will be placed into the query string for each value. + .. versionchanged:: 1.5 + Allow the ``query`` option to be a string to enable alternative + encodings. + If a keyword argument ``anchor`` is present, its string representation will be used as a named anchor in the generated URL (e.g. if ``anchor`` is passed as ``foo`` and the resource URL is @@ -378,8 +398,11 @@ class URLMethodsMixin(object): If ``anchor`` is passed as a string, it should be UTF-8 encoded. If ``anchor`` is passed as a Unicode object, it will be converted to - UTF-8 before being appended to the URL. The anchor value is not - quoted in any way before being appended to the generated URL. + UTF-8 before being appended to the URL. + + .. versionchanged:: 1.5 + The ``anchor`` option will be escaped instead of using + its raw string representation. If both ``anchor`` and ``query`` are specified, the anchor element will always follow the query element, -- cgit v1.2.3 From 9eb79397b4e552bb76bef761593b25c071a616b2 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 27 Nov 2013 03:58:34 -0500 Subject: coverage --- pyramid/tests/test_encode.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyramid/tests/test_encode.py b/pyramid/tests/test_encode.py index 908249877..8fb766d88 100644 --- a/pyramid/tests/test_encode.py +++ b/pyramid/tests/test_encode.py @@ -72,3 +72,8 @@ class URLQuoteTests(unittest.TestCase): la = b'La/Pe\xc3\xb1a' result = self._callFUT(la, '/') self.assertEqual(result, 'La/Pe%C3%B1a') + + def test_it_with_nonstr_nonbinary(self): + la = None + result = self._callFUT(la, '/') + self.assertEqual(result, 'None') -- cgit v1.2.3