diff options
author | Ryan Petrello <lists@ryanpetrello.com> | 2013-04-30 18:27:49 -0400 |
---|---|---|
committer | Ryan Petrello <lists@ryanpetrello.com> | 2013-04-30 21:17:49 -0400 |
commit | df2df2ace56c80cf8b0d89e048d7721951d59ca1 (patch) | |
tree | c2681d353919114cc45905642d1d5e3383700c8f | |
parent | ccf3d8895aebbb111f79829660de0d09c66d14de (diff) | |
download | pecan-df2df2ace56c80cf8b0d89e048d7721951d59ca1.tar.gz |
Fix a Py3 compatability bug in `pecan.jsonify`.
-rw-r--r-- | pecan/compat/__init__.py | 11 | ||||
-rw-r--r-- | pecan/jsonify.py | 3 | ||||
-rw-r--r-- | pecan/secure.py | 100 | ||||
-rw-r--r-- | pecan/tests/test_secure.py | 39 |
4 files changed, 89 insertions, 64 deletions
diff --git a/pecan/compat/__init__.py b/pecan/compat/__init__.py index 3869c12..6efe111 100644 --- a/pecan/compat/__init__.py +++ b/pecan/compat/__init__.py @@ -1,15 +1,15 @@ import sys +import inspect -# True if we are running on Python 3. -PY3 = sys.version_info[0] == 3 +import six -if PY3: # pragma: no cover +if six.PY3: # pragma: no cover text_type = str else: text_type = unicode -if PY3: # pragma: no cover +if six.PY3: # pragma: no cover def native_(s, encoding='latin-1', errors='strict'): """ If ``s`` is an instance of ``text_type``, return ``s``, otherwise return ``str(s, encoding, errors)``""" @@ -31,3 +31,6 @@ return ``str(s, encoding, errors)`` Python 2: If ``s`` is an instance of ``text_type``, return ``s.encode(encoding, errors)``, otherwise return ``str(s)`` """ + +def is_bound_method(ob): + return inspect.ismethod(ob) and six.get_method_self(ob) is not None diff --git a/pecan/jsonify.py b/pecan/jsonify.py index 5bd4c0f..2d89c53 100644 --- a/pecan/jsonify.py +++ b/pecan/jsonify.py @@ -16,6 +16,7 @@ except ImportError: # pragma no cover from webob.multidict import MultiDict webob_dicts = (MultiDict,) +import six from simplegeneric import generic try: @@ -77,7 +78,7 @@ class GenericJSON(JSONEncoder): returns webob_dicts.mixed() dictionary, which is guaranteed to be JSON-friendly. ''' - if hasattr(obj, '__json__') and callable(obj.__json__): + if hasattr(obj, '__json__') and six.callable(obj.__json__): return obj.__json__() elif isinstance(obj, (date, datetime)): return str(obj) diff --git a/pecan/secure.py b/pecan/secure.py index f0a3975..e44aacb 100644 --- a/pecan/secure.py +++ b/pecan/secure.py @@ -1,9 +1,14 @@ from functools import wraps -from inspect import getmembers, ismethod, isfunction +from inspect import getmembers, isfunction from webob import exc import six +if six.PY3: + from .compat import is_bound_method as ismethod +else: + from inspect import ismethod + from .decorators import expose from .util import _cfg, iscontroller @@ -21,6 +26,9 @@ class _SecureState(object): def __nonzero__(self): return self.boolean_value + def __bool__(self): + return self.__nonzero__() + Any = _SecureState('Any', False) Protected = _SecureState('Protected', True) @@ -68,7 +76,7 @@ class _SecuredAttribute(object): def __set_parent(self, parent): if ismethod(parent): - self._parent = parent.im_self + self._parent = six.get_method_self(parent) else: self._parent = parent parent = property(__get_parent, __set_parent) @@ -122,7 +130,7 @@ def secure(func_or_obj, check_permissions_for_obj=None): return _SecuredAttribute(func_or_obj, check_permissions_for_obj) -class SecureController(object): +class SecureControllerMeta(type): """ Used to apply security to a controller. Implementations of SecureController should extend the @@ -130,48 +138,57 @@ class SecureController(object): value (depending on whether or not the user has permissions to the controller). """ - class __metaclass__(type): - def __init__(cls, name, bases, dict_): - cls._pecan = dict( - secured=Protected, - check_permissions=cls.check_permissions, - unlocked=[] - ) - - for name, value in getmembers(cls)[:]: - if ismethod(value): - if iscontroller(value) and value._pecan.get( - 'secured' - ) is None: - # Wrap the function so that the security context is - # local to this class definition. This works around - # the fact that unbound method attributes are shared - # across classes with the same bases. - wrapped = _make_wrapper(value) - wrapped._pecan['secured'] = Protected - wrapped._pecan['check_permissions'] = \ - cls.check_permissions - setattr(cls, name, wrapped) - elif hasattr(value, '__class__'): - if name.startswith('__') and name.endswith('__'): - continue - if isinstance(value, _UnlockedAttribute): - # mark it as unlocked and remove wrapper - cls._pecan['unlocked'].append(value.obj) - setattr(cls, name, value.obj) - elif isinstance(value, _SecuredAttribute): - # The user has specified a different check_permissions - # than the class level version. As far as the class - # is concerned, this method is unlocked because - # it is using a check_permissions function embedded in - # the _SecuredAttribute wrapper - cls._pecan['unlocked'].append(value) + def __init__(cls, name, bases, dict_): + cls._pecan = dict( + secured=Protected, + check_permissions=cls.check_permissions, + unlocked=[] + ) + + for name, value in getmembers(cls)[:]: + if (isfunction if six.PY3 else ismethod)(value): + if iscontroller(value) and value._pecan.get( + 'secured' + ) is None: + # Wrap the function so that the security context is + # local to this class definition. This works around + # the fact that unbound method attributes are shared + # across classes with the same bases. + wrapped = _make_wrapper(value) + wrapped._pecan['secured'] = Protected + wrapped._pecan['check_permissions'] = \ + cls.check_permissions + setattr(cls, name, wrapped) + elif hasattr(value, '__class__'): + if name.startswith('__') and name.endswith('__'): + continue + if isinstance(value, _UnlockedAttribute): + # mark it as unlocked and remove wrapper + cls._pecan['unlocked'].append(value.obj) + setattr(cls, name, value.obj) + elif isinstance(value, _SecuredAttribute): + # The user has specified a different check_permissions + # than the class level version. As far as the class + # is concerned, this method is unlocked because + # it is using a check_permissions function embedded in + # the _SecuredAttribute wrapper + cls._pecan['unlocked'].append(value) + + +class SecureControllerBase(object): @classmethod def check_permissions(cls): return False +SecureController = SecureControllerMeta( + 'SecureController', + (SecureControllerBase,), + {} +) + + def _make_wrapper(f): """return a wrapped function with a copy of the _pecan context""" @wraps(f) @@ -188,7 +205,10 @@ def handle_security(controller): check_permissions = controller._pecan['check_permissions'] if isinstance(check_permissions, six.string_types): - check_permissions = getattr(controller.im_self, check_permissions) + check_permissions = getattr( + six.get_method_self(controller), + check_permissions + ) if not check_permissions(): raise exc.HTTPUnauthorized diff --git a/pecan/tests/test_secure.py b/pecan/tests/test_secure.py index 1f48037..53a63ca 100644 --- a/pecan/tests/test_secure.py +++ b/pecan/tests/test_secure.py @@ -5,6 +5,7 @@ if sys.version_info < (2, 7): else: import unittest # noqa +from six import b as b_ from webtest import TestApp from pecan import expose, make_app @@ -59,11 +60,11 @@ class TestSecure(PecanTestCase): )) response = app.get('/') assert response.status_int == 200 - assert response.body == 'Hello, World!' + assert response.body == b_('Hello, World!') response = app.get('/unlocked') assert response.status_int == 200 - assert response.body == 'Sure thing' + assert response.body == b_('Sure thing') response = app.get('/locked', expect_errors=True) assert response.status_int == 401 @@ -73,7 +74,7 @@ class TestSecure(PecanTestCase): response = app.get('/secret/allowed') assert response.status_int == 200 - assert response.body == 'Allowed!' + assert response.body == b_('Allowed!') def test_unlocked_attribute(self): class AuthorizedSubController(object): @@ -121,11 +122,11 @@ class TestSecure(PecanTestCase): )) response = app.get('/') assert response.status_int == 200 - assert response.body == 'Hello, World!' + assert response.body == b_('Hello, World!') response = app.get('/unlocked') assert response.status_int == 200 - assert response.body == 'Sure thing' + assert response.body == b_('Sure thing') response = app.get('/locked', expect_errors=True) assert response.status_int == 401 @@ -135,15 +136,15 @@ class TestSecure(PecanTestCase): response = app.get('/secret/allowed') assert response.status_int == 200 - assert response.body == 'Allowed!' + assert response.body == b_('Allowed!') response = app.get('/secret/authorized/') assert response.status_int == 200 - assert response.body == 'Index' + assert response.body == b_('Index') response = app.get('/secret/authorized/allowed') assert response.status_int == 200 - assert response.body == 'Allowed!' + assert response.body == b_('Allowed!') def test_secure_attribute(self): authorized = False @@ -163,7 +164,7 @@ class TestSecure(PecanTestCase): app = TestApp(make_app(RootController())) response = app.get('/') assert response.status_int == 200 - assert response.body == 'Hello from root!' + assert response.body == b_('Hello from root!') response = app.get('/sub/', expect_errors=True) assert response.status_int == 401 @@ -171,7 +172,7 @@ class TestSecure(PecanTestCase): authorized = True response = app.get('/sub/') assert response.status_int == 200 - assert response.body == 'Hello from sub!' + assert response.body == b_('Hello from sub!') def test_state_attribute(self): from pecan.secure import Any, Protected @@ -288,7 +289,7 @@ class TestObjectPathSecurity(PecanTestCase): def test_sub_of_both_not_secret(self): response = self.app.get('/notsecret/hi/') assert response.status_int == 200 - assert response.body == 'Index hi' + assert response.body == b_('Index hi') def test_protected_lookup(self): response = self.app.get('/secret/hi/', expect_errors=True) @@ -297,7 +298,7 @@ class TestObjectPathSecurity(PecanTestCase): self.secret_cls.authorized = True response = self.app.get('/secret/hi/') assert response.status_int == 200 - assert response.body == 'Index hi' + assert response.body == b_('Index hi') assert 'secretcontroller' in self.permissions_checked def test_secured_notfound_lookup(self): @@ -324,7 +325,7 @@ class TestObjectPathSecurity(PecanTestCase): self.deepsecret_cls.authorized = True response = self.app.get('/secret/hi/deepsecret/') assert response.status_int == 200 - assert response.body == 'Deep Secret' + assert response.body == b_('Deep Secret') assert 'secretcontroller' in self.permissions_checked assert 'deepsecret' in self.permissions_checked @@ -333,14 +334,14 @@ class TestObjectPathSecurity(PecanTestCase): self.deepsecret_cls.authorized = True response = self.app.get('/secret/1/deepsecret/2/deepsecret/') assert response.status_int == 200 - assert response.body == 'Deep Secret' + assert response.body == b_('Deep Secret') assert 'secretcontroller' in self.permissions_checked assert 'deepsecret' in self.permissions_checked def test_unlocked_lookup(self): response = self.app.get('/notsecret/1/deepsecret/2/') assert response.status_int == 200 - assert response.body == 'Index 2' + assert response.body == b_('Index 2') assert 'deepsecret' not in self.permissions_checked response = self.app.get( @@ -368,7 +369,7 @@ class TestObjectPathSecurity(PecanTestCase): self.secret_cls.independent_authorization = True response = self.app.get('/secret/independent') assert response.status_int == 200 - assert response.body == 'Independent Security' + assert response.body == b_('Independent Security') assert len(self.permissions_checked) == 1 assert 'independent' in self.permissions_checked @@ -383,7 +384,7 @@ class TestObjectPathSecurity(PecanTestCase): self.secret_cls.independent_authorization = True response = self.app.get('/secret/wrapped/') assert response.status_int == 200 - assert response.body == 'Index wrapped' + assert response.body == b_('Index wrapped') assert len(self.permissions_checked) == 1 assert 'independent' in self.permissions_checked @@ -392,7 +393,7 @@ class TestObjectPathSecurity(PecanTestCase): self.secret_cls.independent_authorization = True response = self.app.get('/secret/lookup_wrapped/') assert response.status_int == 200 - assert response.body == 'Index wrapped' + assert response.body == b_('Index wrapped') assert len(self.permissions_checked) == 2 assert 'independent' in self.permissions_checked assert 'secretcontroller' in self.permissions_checked @@ -400,7 +401,7 @@ class TestObjectPathSecurity(PecanTestCase): def test_unlocked_attribute_in_insecure(self): response = self.app.get('/notsecret/unlocked/') assert response.status_int == 200 - assert response.body == 'Index unlocked' + assert response.body == b_('Index unlocked') class SecureControllerSharedPermissionsRegression(PecanTestCase): |