Fix a Py3 compatability bug in pecan.jsonify.

This commit is contained in:
Ryan Petrello
2013-04-30 18:27:49 -04:00
parent ccf3d8895a
commit df2df2ace5
4 changed files with 88 additions and 63 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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=[]
)
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)
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

View File

@@ -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):