From 7742431cc525663d3b65757ef1311cc0afef5ff6 Mon Sep 17 00:00:00 2001 From: Gora Khargosh Date: Sun, 10 Jul 2011 09:10:57 +0530 Subject: [PATCH 01/11] Fixes division operation behavior in create_token() * The function can now accept a numerical base argument. * All usages have been updated to return expected hexadecimal string. * Tests have been added for decimals. * All create_token() tests pass. * ``bytes`` is a Python 2.6+ type not available in Python 2.5. This patch fixes it with a suitable replacement. Signed-off-by: Gora Khargosh --- tests/extras_security_test.py | 12 +++++---- webapp2_extras/security.py | 48 ++++++++++++++++++++++++----------- webapp2_extras/sessions.py | 2 +- 3 files changed, 41 insertions(+), 21 deletions(-) diff --git a/tests/extras_security_test.py b/tests/extras_security_test.py index 9a0a41c..7d37784 100644 --- a/tests/extras_security_test.py +++ b/tests/extras_security_test.py @@ -13,19 +13,21 @@ def test_create_token(self): self.assertRaises(ValueError, security.create_token, 0) self.assertRaises(ValueError, security.create_token, -1) - token = security.create_token(16) + token = security.create_token(16, 16) self.assertTrue(re.match(r'^[a-f0-9]{4}$', token) is not None) - token = security.create_token(32) + token = security.create_token(16, 10) + self.assertTrue(int(token, 10) >= 0) + + token = security.create_token(32, 16) self.assertTrue(re.match(r'^[a-f0-9]{8}$', token) is not None) - token = security.create_token(64) + token = security.create_token(64, 16) self.assertTrue(re.match(r'^[a-f0-9]{16}$', token) is not None) - token = security.create_token(128) + token = security.create_token(128, 16) self.assertTrue(re.match(r'^[a-f0-9]{32}$', token) is not None) - token = security.create_token(16, True) def test_create_check_password_hash(self): self.assertRaises(TypeError, security.create_password_hash, 'foo', diff --git a/webapp2_extras/security.py b/webapp2_extras/security.py index d6c6722..d94289a 100644 --- a/webapp2_extras/security.py +++ b/webapp2_extras/security.py @@ -16,27 +16,45 @@ import webapp2 - -def create_token(bit_strength=64, decimal=False): - """Generates a random string with the specified bit strength. +try: + # Python 2.5 + bytes +except Exception: + # Python 2.6 and above. + bytes = str + + +_BYTE_BASE_ENCODING_MAP = { + 10: lambda value: bytes(int(binascii.b2a_hex(value), 16)), + 16: binascii.b2a_hex, + 64: lambda value: binascii.b2a_base64(value)[:-1] +} +def create_token(bit_strength=64, base=10): + """ + Generates a random ASCII-encoded unsigned integral number in decimal, + hexadecimal, or Base-64 representation. :param bit_strength: - Bit strength. Must be a multiple of 8. - :param decimal: - If True, a decimal representation is returned, otherwise an - hexadecimal representation is returned. + Bit strength. + :param base: + One of: + 1. 10 (default) + 2. 16 + 3. 64 :returns: - A random string with the specified bit strength. + A string representation of a randomly-generated ASCII-encoded + hexadecimal/decimal/base64-representation unsigned integral number + based on the bit strength specified. """ if bit_strength % 8 or bit_strength <= 0: raise ValueError( - 'This function expects a bit strength, got %r.' % bit_strength) - - value = binascii.b2a_hex(os.urandom(bit_strength / 8)) - if decimal: - value = bytes(int(value, 16)) - - return value + "This function expects a bit strength: got `%r`." % bit_strength) + random_bytes = os.urandom(bit_strength // 8) + try: + return _BYTE_BASE_ENCODING_MAP[base](random_bytes) + except KeyError: + raise ValueError( + "Base must be one of %r" % _BYTE_BASE_ENCODING_MAP.keys()) def create_password_hash(password, method='sha1', bit_strength=64, diff --git a/webapp2_extras/sessions.py b/webapp2_extras/sessions.py index d3c8208..9350e75 100644 --- a/webapp2_extras/sessions.py +++ b/webapp2_extras/sessions.py @@ -229,7 +229,7 @@ def _is_valid_sid(self, sid): return sid and self._sid_re.match(sid) is not None def _get_new_sid(self): - return security.create_token(128) + return security.create_token(128, 16) class SessionStore(object): From 9d6e7689cffd39d8bb54d7615a04a91eb4e3496b Mon Sep 17 00:00:00 2001 From: Gora Khargosh Date: Sun, 10 Jul 2011 09:16:21 +0530 Subject: [PATCH 02/11] Updates commentary. Signed-off-by: Gora Khargosh --- webapp2_extras/security.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webapp2_extras/security.py b/webapp2_extras/security.py index d94289a..0c9865e 100644 --- a/webapp2_extras/security.py +++ b/webapp2_extras/security.py @@ -17,10 +17,10 @@ import webapp2 try: - # Python 2.5 + # Python 2.6 bytes except Exception: - # Python 2.6 and above. + # Python 2.5 bytes = str From 74d9781e24659a1b77faf51afda1017b9b0440cb Mon Sep 17 00:00:00 2001 From: Gora Khargosh Date: Sun, 10 Jul 2011 09:32:01 +0530 Subject: [PATCH 03/11] Fixes issues with the json module. * You cannot name a module "json" and import "json" from within it. This import test had been failing all this time. The module has been renamed to "escape" for all the tests to work. * Stray modules (also because they are named "json") can pollute the Python namespace and cause our imports to fail. We're expecting Python's json module, but we can end up receiving a stray module named json. Therefore, we test whether we're using Python's module. * All tests now pass. Signed-off-by: Gora Khargosh --- tests/extras_json_test.py | 3 ++- webapp2_extras/{json.py => escape.py} | 22 +++++++++++++++++++--- webapp2_extras/securecookie.py | 2 +- 3 files changed, 22 insertions(+), 5 deletions(-) rename webapp2_extras/{json.py => escape.py} (77%) diff --git a/tests/extras_json_test.py b/tests/extras_json_test.py index 917848a..cb80d85 100644 --- a/tests/extras_json_test.py +++ b/tests/extras_json_test.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- -from webapp2_extras import json + +from webapp2_extras import escape as json import test_base diff --git a/webapp2_extras/json.py b/webapp2_extras/escape.py similarity index 77% rename from webapp2_extras/json.py rename to webapp2_extras/escape.py index 33256bb..d93a899 100644 --- a/webapp2_extras/json.py +++ b/webapp2_extras/escape.py @@ -14,14 +14,30 @@ try: # Preference for installed library with updated fixes. import simplejson as json + json_loads = json.loads + json_dumps = json.dumps except ImportError: # pragma: no cover try: # Standard library module in Python >= 2.6. + # If you name this module as "json", this import + # will fail *always*. Therefore, this module has been renamed + # to "escape". import json - except ImportError: + + # Stray modules from other libraries named "json" can cause + # "loads" or "dumps" attributes missing errors. For instance, + # all the JSON tests are failing on my machine with Python 2.7. + # We use the Python built-in json module only if it has the + # attributes we need. See tornado commit log. + assert hasattr(json, "loads") and hasattr(json, "dumps") + json_loads = json.loads + json_dumps = json.dumps + except Exception: try: # Google App Engine. from django.utils import simplejson as json + json_loads = json.loads + json_dumps = json.dumps except ImportError: raise RuntimeError( 'A JSON parser is required, e.g., simplejson at ' @@ -49,7 +65,7 @@ def encode(value, *args, **kwargs): # although python's standard library does not, so we do it here. # http://stackoverflow.com/questions/1580647/json-why-are-forward-slashes-escaped kwargs.setdefault('separators', (',', ':')) - return json.dumps(value, *args, **kwargs).replace(" Date: Sun, 10 Jul 2011 10:24:17 +0530 Subject: [PATCH 04/11] Makes urlparse imports portable. Signed-off-by: Gora Khargosh --- webapp2.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/webapp2.py b/webapp2.py index bef33cd..6364ecf 100755 --- a/webapp2.py +++ b/webapp2.py @@ -14,7 +14,13 @@ import logging import re import urllib -import urlparse + +try: + # Python 3. + from urllib.parse import urljoin, urlunsplit +except ImportError: + # Python 2.x + from urlparse import urljoin, urlunsplit import webob from webob import exc @@ -1644,7 +1650,7 @@ def redirect(uri, permanent=False, abort=False, code=None, body=None, """ if uri.startswith(('.', '/')): request = request or get_request() - uri = str(urlparse.urljoin(request.url, uri)) + uri = str(urljoin(request.url, uri)) if code is None: if permanent: @@ -1772,7 +1778,7 @@ def _urlunsplit(scheme=None, netloc=None, path=None, query=None, if fragment: fragment = urllib.quote(_to_utf8(fragment)) - return urlparse.urlunsplit((scheme, netloc, path, query, fragment)) + return urlunsplit((scheme, netloc, path, query, fragment)) def _get_handler_methods(handler): From 824cb1a822130604dc71012af84f12606190dc23 Mon Sep 17 00:00:00 2001 From: Gora Khargosh Date: Sun, 10 Jul 2011 10:34:47 +0530 Subject: [PATCH 05/11] Moves webapp2.py into webapp2/__init__.py * Everything in a single module seems fine for a start but gets unweildy very fast. Signed-off-by: Gora Khargosh --- webapp2.py => webapp2/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename webapp2.py => webapp2/__init__.py (100%) diff --git a/webapp2.py b/webapp2/__init__.py similarity index 100% rename from webapp2.py rename to webapp2/__init__.py From 7fadf720308b2feefb9ad6db40151ef329c0861c Mon Sep 17 00:00:00 2001 From: Gora Khargosh Date: Sun, 10 Jul 2011 10:38:13 +0530 Subject: [PATCH 06/11] Removes executable permissions from webapp2/__init__.py Signed-off-by: Gora Khargosh --- webapp2/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 webapp2/__init__.py diff --git a/webapp2/__init__.py b/webapp2/__init__.py old mode 100755 new mode 100644 From 9d672cc3164a9d89c53f72a04314adcecbd66ead Mon Sep 17 00:00:00 2001 From: Gora Khargosh Date: Sun, 10 Jul 2011 10:45:40 +0530 Subject: [PATCH 07/11] Adds ``types`` module to transparently convert between types. Signed-off-by: Gora Khargosh --- webapp2/types.py | 191 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 webapp2/types.py diff --git a/webapp2/types.py b/webapp2/types.py new file mode 100644 index 0000000..aded5b9 --- /dev/null +++ b/webapp2/types.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Type conversion utilities. +# +# Copyright (C) 2009 Facebook +# Copyright (C) 2011 tipfy.org +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +:module: webapp2.types +:synopsis: Common portable Python type conversion and detection. + +Type detection +-------------- +.. autofunction:: is_sequence +.. autofunction:: is_unicode +.. autofunction:: is_bytes +.. autofunction:: is_bytes_or_unicode +.. autofunction:: unicode_to_utf8 +.. autofunction:: bytes_to_unicode +.. autofunction:: to_utf8_if_unicode +.. autofunction:: to_unicode_if_bytes +.. autofunction:: to_unicode_recursively +""" + + +try: + # Python 2.6 + + bytes = bytes +except Exception: + # Python 2.5 does not have a built in bytes type. + bytes = str + +try: + # Not Python3 + unicode_string = unicode +except Exception: + # Python3. + unicode_string = str + basestring = (str, bytes) + + +def is_sequence(value): + """ + Determines whether the given value is a sequence. + + :param value: + The value to test. + :returns: + ``True`` if the value is a sequence; ``False`` otherwise. + """ + try: + list(value) + return True + except TypeError, exception: + assert "is not iterable" in bytes(exception) + return False + + +def is_unicode(value): + """ + Determines whether the given value is a Unicode string. + + :param value: + The value to test. + :returns: + ``True`` if ``value`` is a Unicode string; ``False`` otherwise. + """ + return isinstance(value, unicode_string) + + +def is_bytes(value): + """ + Determines whether the given value is a byte string. + + :param value: + The value to test. + :returns: + ``True`` if ``value`` is a byte string; ``False`` otherwise. + """ + return isinstance(value, bytes) + + +def is_bytes_or_unicode(value): + """ + Determines whether the given value is an instance of a string irrespective + of whether it is a byte string or a Unicode string. + + :param value: + The value to test. + :returns: + ``True`` if ``value`` is a string; ``False`` otherwise. + """ + return isinstance(value, basestring) + + +def unicode_to_utf8(value): + """ + Converts a string argument to a UTF-8 encoded byte string if it is a + Unicode string. + + :param value: + If already a byte string or None, it is returned unchanged. + Otherwise it must be a Unicode string and is encoded as UTF-8. + """ + if value is None or is_bytes(value): + return value + assert is_unicode(value) + return value.encode("utf-8") + + +def bytes_to_unicode(value, encoding="utf-8"): + """ + Converts bytes to a Unicode string decoding it according to the encoding + specified. + + :param value: + If already a Unicode string or None, it is returned unchanged. + Otherwise it must be a byte string. + :param encoding: + The encoding used to decode bytes. Defaults to UTF-8 + """ + if value is None or is_unicode(value): + return value + assert is_bytes(value) + return value.decode(encoding) + + +def to_utf8_if_unicode(value): + """ + Converts an argument to a UTF-8 encoded byte string if the argument + is a Unicode string. + + :param value: + The value that will be UTF-8 encoded if it is a string. + :returns: + UTF-8 encoded byte string if the argument is a Unicode string; otherwise + the value is returned unchanged. + """ + return unicode_to_utf8(value) if is_unicode(value) else value + + +def to_unicode_if_bytes(value, encoding="utf-8"): + """ + Converts an argument to Unicode string if the argument is a byte string + decoding it as specified by the encoding. + + :param value: + The value that will be converted to a Unicode string. + :param encoding: + The encoding used to decode bytes. Defaults to UTF-8. + :returns: + Unicode string if the argument is a byte string. Otherwise the value + is returned unchanged. + """ + return bytes_to_unicode(value, encoding) if is_bytes(value) else value + + +def to_unicode_recursively(obj): + """ + Walks a simple data structure, converting byte strings to unicode. + + Supports lists, tuples, and dictionaries. + + :param obj: + The object to walk. + :returns: + obj with all byte strings converted into Unicode. + """ + if isinstance(obj, dict): + return dict((to_unicode_recursively(k), + to_unicode_recursively(v)) for (k, v) in obj.iteritems()) + elif isinstance(obj, list): + return list(to_unicode_recursively(i) for i in obj) + elif isinstance(obj, tuple): + return tuple(to_unicode_recursively(i) for i in obj) + elif is_bytes(obj): + return bytes_to_unicode(obj) + else: + return obj From 784d55cbc0edab3e86262de0f03daacf30727ee1 Mon Sep 17 00:00:00 2001 From: Gora Khargosh Date: Sun, 10 Jul 2011 10:46:15 +0530 Subject: [PATCH 08/11] Use ``bytes()`` instead of ``str()`` wherever byte strings are required. Signed-off-by: Gora Khargosh --- webapp2/__init__.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/webapp2/__init__.py b/webapp2/__init__.py index 6364ecf..014b14a 100644 --- a/webapp2/__init__.py +++ b/webapp2/__init__.py @@ -50,6 +50,10 @@ def _run(self, app): run_wsgi_app = run_bare_wsgi_app = classmethod(_run) + +from webapp2.types import bytes + + __version_info__ = ('1', '8', '1') __version__ = '.'.join(__version_info__) @@ -244,7 +248,7 @@ def blank(cls, path, environ=None, base_url=None, data = urllib.urlencode(data) environ['wsgi.input'] = StringIO(data) environ['webob.is_body_seekable'] = True - environ['CONTENT_LENGTH'] = str(len(data)) + environ['CONTENT_LENGTH'] = bytes(len(data)) environ['CONTENT_TYPE'] = 'application/x-www-form-urlencoded' base = super(Request, cls).blank(path, environ=environ, @@ -349,7 +353,7 @@ def _set_status(self, value): else: if isinstance(value, unicode): # Status messages have to be ASCII safe, so this is OK. - value = str(value) + value = bytes(value) if not isinstance(value, str): raise TypeError( @@ -430,7 +434,7 @@ def wsgi_write(self, start_response): if (self.headers.get('Cache-Control') == 'no-cache' and not self.headers.get('Expires')): self.headers['Expires'] = 'Fri, 01 Jan 1990 00:00:00 GMT' - self.headers['Content-Length'] = str(len(self.body)) + self.headers['Content-Length'] = bytes(len(self.body)) write = start_response(self.status, self.headerlist) write(self.body) @@ -981,7 +985,7 @@ def _build(self, args, kwargs): name.strip('_')) if not isinstance(value, basestring): - value = str(value) + value = bytes(value) if not regex.match(value): raise ValueError('URI buiding error: Value "%s" is not ' @@ -1650,7 +1654,7 @@ def redirect(uri, permanent=False, abort=False, code=None, body=None, """ if uri.startswith(('.', '/')): request = request or get_request() - uri = str(urljoin(request.url, uri)) + uri = bytes(urljoin(request.url, uri)) if code is None: if permanent: From 281f34a211f85f961d7b64f0527c06d6c1ad8c03 Mon Sep 17 00:00:00 2001 From: Gora Khargosh Date: Sun, 10 Jul 2011 11:06:23 +0530 Subject: [PATCH 09/11] All tests pass with portable byte string/unicode string conversion. Signed-off-by: Gora Khargosh --- tests/misc_test.py | 4 +-- webapp2/__init__.py | 50 +++++++++++++++++--------------------- webapp2/types.py | 43 ++++++++++++++++++++++++++++++++ webapp2_extras/security.py | 14 +++-------- 4 files changed, 71 insertions(+), 40 deletions(-) diff --git a/tests/misc_test.py b/tests/misc_test.py index 5390429..15a7469 100644 --- a/tests/misc_test.py +++ b/tests/misc_test.py @@ -64,10 +64,10 @@ def test_import_string(self): self.assertRaises(AttributeError, webapp2.import_string, 'webob.dfasfasdfdsfsd') def test_to_utf8(self): - res = webapp2._to_utf8('ábcdéf'.decode('utf-8')) + res = webapp2.types.unicode_to_utf8('ábcdéf'.decode('utf-8')) self.assertEqual(isinstance(res, str), True) - res = webapp2._to_utf8('abcdef') + res = webapp2.types.unicode_to_utf8('abcdef') self.assertEqual(isinstance(res, str), True) ''' diff --git a/webapp2/__init__.py b/webapp2/__init__.py index 014b14a..3b4ec4d 100644 --- a/webapp2/__init__.py +++ b/webapp2/__init__.py @@ -50,9 +50,9 @@ def _run(self, app): run_wsgi_app = run_bare_wsgi_app = classmethod(_run) - -from webapp2.types import bytes - +from webapp2.types import \ + bytes, is_bytes, is_bytes_or_unicode, bytes_to_unicode, \ + is_unicode, is_list, is_tuple, is_dict, unicode_to_utf8, to_unicode_if_bytes, unicode_string __version_info__ = ('1', '8', '1') __version__ = '.'.join(__version_info__) @@ -244,7 +244,7 @@ def blank(cls, path, environ=None, base_url=None, environ['REQUEST_METHOD'] = 'POST' if hasattr(data, 'items'): data = data.items() - if not isinstance(data, str): + if not is_bytes(data): data = urllib.urlencode(data) environ['wsgi.input'] = StringIO(data) environ['webob.is_body_seekable'] = True @@ -336,10 +336,12 @@ def write(self, text): """Appends a text to the response body.""" # webapp uses StringIO as Response.out, so we need to convert anything # that is not str or unicode to string to keep same behavior. - if not isinstance(text, basestring): - text = unicode(text) + #if not is_bytes_or_unicode(text): + # text = bytes(text) + if not is_bytes_or_unicode(text): + text = unicode_string(text) - if isinstance(text, unicode) and not self.charset: + if is_unicode(text) and not self.charset: self.charset = self.default_charset super(Response, self).write(text) @@ -351,11 +353,11 @@ def _set_status(self, value): if isinstance(value, (int, long)): code = int(value) else: - if isinstance(value, unicode): + if is_unicode(value): # Status messages have to be ASCII safe, so this is OK. value = bytes(value) - if not isinstance(value, str): + if not is_bytes(value): raise TypeError( 'You must set status to a string or integer (not %s)' % type(value)) @@ -407,7 +409,7 @@ def _get_headers(self): def _set_headers(self, value): if hasattr(value, 'items'): value = value.items() - elif not isinstance(value, list): + elif not is_list(value): raise TypeError('Response headers must be a list or dictionary.') self.headerlist = value @@ -905,7 +907,7 @@ def __init__(self, template, handler=None, name=None, defaults=None, self.defaults = defaults or {} self.methods = methods self.schemes = schemes - if isinstance(handler, basestring) and ':' in handler: + if is_bytes_or_unicode(handler) and ':' in handler: if handler_method: raise ValueError( "If handler_method is defined in a Route, handler " @@ -984,7 +986,7 @@ def _build(self, args, kwargs): raise KeyError('Missing argument "%s" to build URI.' % \ name.strip('_')) - if not isinstance(value, basestring): + if not is_bytes_or_unicode(value): value = bytes(value) if not regex.match(value): @@ -1102,7 +1104,7 @@ def add(self, route): A :class:`Route` instance or, for compatibility with webapp, a tuple ``(regex, handler_class)``. """ - if isinstance(route, tuple): + if is_tuple(route): # Exceptional compatibility case: route compatible with webapp. route = self.route_class(*route) @@ -1234,7 +1236,7 @@ def default_dispatcher(self, request, response): if route.handler_adapter is None: handler = route.handler - if isinstance(handler, basestring): + if is_bytes_or_unicode(handler): if handler not in self.handlers: self.handlers[handler] = handler = import_string(handler) else: @@ -1537,7 +1539,7 @@ def handle_exception(self, request, response, e): handler = self.error_handlers.get(code) if handler: - if isinstance(handler, basestring): + if is_bytes_or_unicode(handler): self.error_handlers[code] = handler = import_string(handler) return handler(request, response, e) @@ -1734,7 +1736,7 @@ def import_string(import_name, silent=False): :returns: The imported object. """ - import_name = _to_utf8(import_name) + import_name = unicode_to_utf8(import_name) try: if '.' in import_name: module, obj = import_name.rsplit('.', 1) @@ -1770,17 +1772,17 @@ def _urlunsplit(scheme=None, netloc=None, path=None, query=None, netloc = None if path: - path = urllib.quote(_to_utf8(path)) + path = urllib.quote(unicode_to_utf8(path)) - if query and not isinstance(query, basestring): - if isinstance(query, dict): + if query and not is_bytes_or_unicode(query): + if is_dict(query): query = query.iteritems() # Sort args: commonly needed to build signatures for services. query = urllib.urlencode(sorted(query)) if fragment: - fragment = urllib.quote(_to_utf8(fragment)) + fragment = urllib.quote(unicode_to_utf8(fragment)) return urlunsplit((scheme, netloc, path, query, fragment)) @@ -1806,14 +1808,6 @@ def _normalize_handler_method(method): return method.lower().replace('-', '_') -def _to_utf8(value): - """Encodes a unicode value to UTF-8 if not yet encoded.""" - if isinstance(value, str): - return value - - return value.encode('utf-8') - - def _parse_route_template(template, default_sufix=''): """Lazy route template parser.""" variables = {} diff --git a/webapp2/types.py b/webapp2/types.py index aded5b9..cc0f095 100644 --- a/webapp2/types.py +++ b/webapp2/types.py @@ -24,6 +24,9 @@ Type detection -------------- .. autofunction:: is_sequence +.. autofunction:: is_list +.. autofunction:: is_tuple +.. autofunction:: is_dict .. autofunction:: is_unicode .. autofunction:: is_bytes .. autofunction:: is_bytes_or_unicode @@ -34,6 +37,9 @@ .. autofunction:: to_unicode_recursively """ +# Essentially, avoid using "str", "basestring", and "unicode". +# Use these types instead. They're portable between Python versions. + try: # Python 2.6 + @@ -42,6 +48,7 @@ # Python 2.5 does not have a built in bytes type. bytes = str +# Python 3.x also removes "basestring" and "unicode" try: # Not Python3 unicode_string = unicode @@ -68,6 +75,42 @@ def is_sequence(value): return False +def is_list(value): + """ + Determines whether the given value is a list. + + :param value: + The value to test. + :returns: + ``True`` if the value is a list instance; ``False`` otherwise. + """ + return isinstance(value, list) + + +def is_tuple(value): + """ + Determines whether the given value is a tuple. + + :param value: + The value to test. + :returns: + ``True`` if the value is a tuple instance; ``False`` otherwise. + """ + return isinstance(value, tuple) + + +def is_dict(value): + """ + Determines whether the given value is a dict. + + :param value: + The value to test. + :returns: + ``True`` if the value is a dict instance; ``False`` otherwise. + """ + return isinstance(value, dict) + + def is_unicode(value): """ Determines whether the given value is a Unicode string. diff --git a/webapp2_extras/security.py b/webapp2_extras/security.py index 0c9865e..4b0f6e2 100644 --- a/webapp2_extras/security.py +++ b/webapp2_extras/security.py @@ -16,13 +16,7 @@ import webapp2 -try: - # Python 2.6 - bytes -except Exception: - # Python 2.5 - bytes = str - +from webapp2.types import bytes, unicode_to_utf8 _BYTE_BASE_ENCODING_MAP = { 10: lambda value: bytes(int(binascii.b2a_hex(value), 16)), @@ -134,7 +128,7 @@ def hash_password(password, method, salt=None, pepper=None): This function was ported and adapted from `Werkzeug`_. """ - password = webapp2._to_utf8(password) + password = unicode_to_utf8(password) if method == 'plain': return password @@ -143,12 +137,12 @@ def hash_password(password, method, salt=None, pepper=None): return None if salt: - h = hmac.new(webapp2._to_utf8(salt), password, method) + h = hmac.new(unicode_to_utf8(salt), password, method) else: h = method(password) if pepper: - h = hmac.new(webapp2._to_utf8(pepper), h.hexdigest(), method) + h = hmac.new(unicode_to_utf8(pepper), h.hexdigest(), method) return h.hexdigest() From 6c94cf01ba350bcfd4d013d82bca4d2d75df4ced Mon Sep 17 00:00:00 2001 From: Gora Khargosh Date: Sun, 10 Jul 2011 11:08:02 +0530 Subject: [PATCH 10/11] Ignore editor specific files. Signed-off-by: Gora Khargosh --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 8dbb815..b4c34da 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ # repository level. *.egg-info +# Editor-specific files. +.idea/ From 30a7a250bafa5acfce60e3bc9f169a7a9e5d0fda Mon Sep 17 00:00:00 2001 From: Gora Khargosh Date: Sun, 10 Jul 2011 11:22:23 +0530 Subject: [PATCH 11/11] Adds ``types`` module justification to its documentation. Signed-off-by: Gora Khargosh --- webapp2/types.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/webapp2/types.py b/webapp2/types.py index cc0f095..fa67a3f 100644 --- a/webapp2/types.py +++ b/webapp2/types.py @@ -21,6 +21,37 @@ :module: webapp2.types :synopsis: Common portable Python type conversion and detection. +``bytes``, ``str``, ``unicode``, and ``basestring`` mean different +things to Python 2.5, 2.6, and 3.x. + +Python 2.5 +* ``bytes`` is not available. +* ``str`` is a byte string. +* ``unicode`` converts to unicode string. +* ``basestring`` exists. + +Python 2.6 +* ``bytes`` is available and maps to str +* ``str`` is a byte string. +* ``unicode`` converts to unicode string +* ``basestring`` exists. + +Python 3.x +* ``bytes`` is available and does not map to ``str``. +* ``str`` maps to the earlier ``unicode``, but ``unicode`` has been removed. +* ``basestring`` has been removed. +* ``unicode`` has been removed + +This module adds portable support for all three versions +of Python. It introduces these portable types that you can use +in your code: + +* ``bytes`` where you need byte strings. +* ``unicode_string`` where you need unicode strings +* a few other utility functions that hide all the + complications behind type checking therefore cleaning + up the code base. + Type detection -------------- .. autofunction:: is_sequence