diff --git a/.pydevproject b/.pydevproject index af8c4ef..9948920 100644 --- a/.pydevproject +++ b/.pydevproject @@ -2,8 +2,8 @@ -Default -python 2.7 +python3 +python 3.0 /Milla/src diff --git a/setup.py b/setup.py index 9497f7f..1c7cf2f 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ if sys.version_info < (2, 7): setup( name='Milla', - version='tip', + version='py3k', description='Lightweight WSGI framework for web applications', long_description='''\ Milla is a simple WSGI framework for Python web applications. It is mostly diff --git a/src/milla/__init__.py b/src/milla/__init__.py index bef31c3..f8334b3 100644 --- a/src/milla/__init__.py +++ b/src/milla/__init__.py @@ -1,11 +1,11 @@ # Copyright 2011 Dustin C. Hatch -# +# # 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. @@ -19,8 +19,13 @@ from milla.app import * from milla.auth.decorators import * from webob.exc import * import webob -import urllib -import urlparse +try: + import urllib.parse +except ImportError: #pragma: no cover + import urllib + import urlparse + urllib.parse = urlparse + urllib.parse.urlencode = urllib.urlencode #@UndefinedVariable def allow(*methods): '''Specify the allowed HTTP verbs for a controller callable @@ -77,7 +82,7 @@ class Request(webob.Request): url = self._merge_url(self.script_name, path) if keywords: - url += '?' + urllib.urlencode(keywords) + url += '?' + urllib.parse.urlencode(keywords) return url @@ -97,7 +102,7 @@ class Request(webob.Request): url = self._merge_url(self.application_url, path) if keywords: - url += '?' + urllib.urlencode(keywords) + url += '?' + urllib.parse.urlencode(keywords) return url @@ -141,4 +146,4 @@ class Request(webob.Request): if not root.endswith('/'): root += '/' - return urlparse.urljoin(root, path) + return urllib.parse.urljoin(root, path) diff --git a/src/milla/app.py b/src/milla/app.py index 3250a90..3dcb944 100644 --- a/src/milla/app.py +++ b/src/milla/app.py @@ -25,7 +25,6 @@ from milla.controllers import FaviconController from milla.util import asbool from webob.exc import HTTPNotFound, WSGIHTTPException, HTTPMethodNotAllowed import milla.dispatch.traversal -import webob __all__ = ['Application'] @@ -93,50 +92,54 @@ class Application(object): start_response_wrapper = StartResponseWrapper(start_response) request.start_response = start_response_wrapper try: - # If the callable has an __before__ attribute, call it - if hasattr(func, '__before__'): - func.__before__(request) - # If the callable is an instance method and its class has - # a __before__ method, call that - elif hasattr(func, 'im_self') and \ - hasattr(func.im_self, '__before__'): - func.im_self.__before__(request) - # The callable might be a partial, so check the inner func - elif hasattr(func, 'func'): - if hasattr(func.func, '__before__'): - func.func.__before__(request) - elif hasattr(func.func, 'im_self') and \ - hasattr(func.func.im_self, '__before__'): - func.func.im_self.__before__(request) + self._call_before(func)(request) response = func(request) except WSGIHTTPException as e: return e(environ, start_response) finally: - # If the callable has an __after__ method, call it - if hasattr(func, '__after__'): - func.__after__(request) - # If the callable is an instance method and its class has - # an __after__ method, call that - elif hasattr(func, 'im_self') and \ - hasattr(func.im_self, '__after__'): - func.im_self.__after__(request) - # The callable might be a partial, so check the inner func - elif hasattr(func, 'func'): - if hasattr(func.func, '__after__'): - func.func.__after__(request) - elif hasattr(func.func, 'im_self') and \ - hasattr(func.func.im_self, '__after__'): - func.func.im_self.__after__(request) + self._call_after(func)(request) # The callable might have returned just a string, which is OK, - # but we need to wrap it in a WebOb response + # but we need to wrap it in a Response object + try: + # In Python 2, it could be a str or a unicode object + basestring = basestring #@UndefinedVariable + except NameError: + # Python 3 has no unicode objects and thus no need for + # basestring so we, just make it an alias for str + basestring = str if isinstance(response, basestring) or not response: - response = webob.Response(response) + response = request.ResponseClass(response) if not start_response_wrapper.called: start_response(response.status, response.headerlist) if not environ['REQUEST_METHOD'] == 'HEAD': return response.app_iter + + def _call_after(self, func): + try: + return self._find_attr(func, '__after__') + except AttributeError: + return lambda r: None + + def _call_before(self, func): + try: + return self._find_attr(func, '__before__') + except AttributeError: + return lambda r: None + + def _find_attr(self, obj, attr): + try: + # Object has the specified attribute itself + return getattr(obj, attr) + except AttributeError: + # Object is a bound method; look for the attribute on the instance + if hasattr(obj, '__self__'): + return self._find_attr(obj.__self__, attr) + # Object is a partial; look for the attribute on the inner function + elif hasattr(obj, 'func'): + return self._find_attr(obj.func, attr) + raise class StartResponseWrapper(): diff --git a/src/milla/auth/__init__.py b/src/milla/auth/__init__.py index 8909fab..daeb535 100644 --- a/src/milla/auth/__init__.py +++ b/src/milla/auth/__init__.py @@ -37,7 +37,7 @@ class NotAuthorized(Exception): All other arguments and keywords are ignored. ''' - response = request.ResponseClass(unicode(self)) + response = request.ResponseClass(str(self)) response.status_int = 403 return response diff --git a/src/milla/auth/decorators.py b/src/milla/auth/decorators.py index edb8780..d40c759 100644 --- a/src/milla/auth/decorators.py +++ b/src/milla/auth/decorators.py @@ -124,7 +124,7 @@ class require_perms(object): def __init__(self, *requirements): requirement = None for req in requirements: - if isinstance(req, basestring): + if not hasattr(req, 'check'): req = permissions.Permission(req) if not requirement: requirement = req diff --git a/src/milla/auth/permissions.py b/src/milla/auth/permissions.py index 00a78e0..553285d 100644 --- a/src/milla/auth/permissions.py +++ b/src/milla/auth/permissions.py @@ -86,17 +86,8 @@ class Permission(BasePermission): def __init__(self, name): self.name = name - def __unicode__(self): - if isinstance(self.name, unicode): - return self.name - else: - return self.name.decode('utf-8') - def __str__(self): - if isinstance(self.name, str): - return self.name - else: - return self.name.encode('utf-8') + return str(self.name) def __eq__(self, other): return self is other or str(self) == str(other) @@ -119,7 +110,7 @@ class PermissionRequirement(BasePermission): self.requirements = requirements def __str__(self): - return unicode(self).encode('utf-8') + return ', '.join(self.requirements) class PermissionRequirementAll(PermissionRequirement): '''Complex permission requirement needing all given permissions''' diff --git a/src/milla/config.py b/src/milla/config.py index f656411..f50790f 100644 --- a/src/milla/config.py +++ b/src/milla/config.py @@ -40,8 +40,14 @@ def read_config(filename): app.config.update(config) ''' - cparser = configparser.SafeConfigParser() - cparser.readfp(open(filename)) + with open(filename) as f: + # ConfigParser API changed in Python 3.2 + if hasattr(configparser.ConfigParser, 'read_file'): + cparser = configparser.ConfigParser() + cparser.read_file(f) + else: + cparser = configparser.SafeConfigParser() + cparser.readfp(f) config = {} for section in cparser.sections(): diff --git a/src/milla/dispatch/routing.py b/src/milla/dispatch/routing.py index d0d7e02..239ead7 100644 --- a/src/milla/dispatch/routing.py +++ b/src/milla/dispatch/routing.py @@ -200,7 +200,7 @@ class Router(object): module ``some.module``. ''' - if isinstance(controller, basestring): + if not hasattr(controller, '__call__'): controller = self._import_controller(controller) self.routes.append((self._compile_template(template), controller, vars)) diff --git a/src/milla/tests/test_app.py b/src/milla/tests/test_app.py index cd5b1d5..f42f0ca 100644 --- a/src/milla/tests/test_app.py +++ b/src/milla/tests/test_app.py @@ -33,7 +33,7 @@ class ResponseMaker(object): def __init__(self, http_version=1.1): self.http_version = http_version self.headers = '' - self.body = '' + self.body = b'' def start_response(self, status, response_headers): self.headers += 'HTTP/{0} {1}\r\n'.format(self.http_version, status) @@ -78,7 +78,7 @@ def test_favicon(): app_iter = app(environ, response.start_response) response.finish_response(app_iter) assert response.headers.startswith('HTTP/1.1 200'), response.headers - assert response.body.startswith('\x00\x00\x01\x00'), response.body + assert response.body.startswith(b'\x00\x00\x01\x00'), response.body def test_allow_header_disallowed(): '''HTTP 405 is returned for disallowed HTTP request methods @@ -138,7 +138,7 @@ def test_emulated_method(): }) body = environ['wsgi.input'] body.seek(0) - body.write('_method=PUT') + body.write(b'_method=PUT') body.seek(0) response = ResponseMaker() app_iter = app(environ, response.start_response) @@ -358,7 +358,7 @@ def test_httperror_response(): ''' def controller(request): - raise webob.exc.HTTPClientError() + raise webob.exc.HTTPClientError('NotFound') app = milla.app.Application(StubResolver(controller)) environ = environ_for_testing() @@ -366,7 +366,7 @@ def test_httperror_response(): app_iter = app(environ, response.start_response) response.finish_response(app_iter) assert response.headers.startswith('HTTP/1.1 400'), response.headers - assert webob.exc.HTTPClientError.explanation in response.body, response.body + assert b'NotFound' in response.body, response.body def test_single_start_response(): '''Ensure start_response is only called once''' @@ -393,7 +393,7 @@ def test_single_start_response(): response.finish_response(app_iter) assert start_response.call_count == 1, start_response.call_count assert response.headers.startswith('HTTP/1.1 200 OK'), response.headers - assert response.body == 'test', response.body + assert response.body == b'test', response.body def test_allow_decorator(): '''Ensure allow decorator sets allowed_methods on controllers'''