diff options
| author | Martin <martin.frlin@gmail.com> | 2016-12-06 12:07:38 +0100 |
|---|---|---|
| committer | Martin <martin.frlin@gmail.com> | 2016-12-06 12:07:38 +0100 |
| commit | 33a048d505f690bde6c44914dff154abfc2e790f (patch) | |
| tree | 52d0f892e5bd769b3942f860cb99bd03f3ec96ed | |
| parent | 8bb7a4ba3593895a12ded27260ece7c1d673de56 (diff) | |
| parent | fedfacf0bd1cbefbe922618d4df93917465f1307 (diff) | |
| download | pyramid-33a048d505f690bde6c44914dff154abfc2e790f.tar.gz pyramid-33a048d505f690bde6c44914dff154abfc2e790f.tar.bz2 pyramid-33a048d505f690bde6c44914dff154abfc2e790f.zip | |
Merge remote-tracking branch 'upstream/master' into issue-2656
| -rw-r--r-- | CONTRIBUTORS.txt | 2 | ||||
| -rw-r--r-- | docs/conf.py | 4 | ||||
| -rw-r--r-- | pyramid/config/predicates.py | 303 | ||||
| -rw-r--r-- | pyramid/config/routes.py | 10 | ||||
| -rw-r--r-- | pyramid/config/security.py | 2 | ||||
| -rw-r--r-- | pyramid/config/tweens.py | 2 | ||||
| -rw-r--r-- | pyramid/config/util.py | 15 | ||||
| -rw-r--r-- | pyramid/config/views.py | 6 | ||||
| -rw-r--r-- | pyramid/predicates.py | 300 | ||||
| -rw-r--r-- | pyramid/tests/test_config/test_util.py | 11 | ||||
| -rw-r--r-- | pyramid/tests/test_predicates.py (renamed from pyramid/tests/test_config/test_predicates.py) | 22 | ||||
| -rw-r--r-- | pyramid/util.py | 15 |
12 files changed, 350 insertions, 342 deletions
diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 98e243c1f..b4e30e085 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -286,3 +286,5 @@ Contributors - Keith Yang, 2016/07/22 - Moriyoshi Koizumi, 2016/11/20 + +- Mikko Ohtamaa, 2016/12/6 diff --git a/docs/conf.py b/docs/conf.py index c3a7170fc..84fd5cf1b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -191,10 +191,10 @@ latex_documents = [ # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -latex_use_parts = True +latex_toplevel_sectioning = "section" # If false, no module index is generated. -latex_use_modindex = False +latex_domain_indices = False ## Say, for a moment that you have a twoside document that needs a 3cm ## inner margin to allow for binding and at least two centimetres the diff --git a/pyramid/config/predicates.py b/pyramid/config/predicates.py index 0b76bbd70..5d99d6564 100644 --- a/pyramid/config/predicates.py +++ b/pyramid/config/predicates.py @@ -1,301 +1,2 @@ -import re - -from pyramid.exceptions import ConfigurationError - -from pyramid.compat import is_nonstr_iter - -from pyramid.traversal import ( - find_interface, - traversal_path, - resource_path_tuple - ) - -from pyramid.urldispatch import _compile_route -from pyramid.util import object_description -from pyramid.session import check_csrf_token - -from .util import as_sorted_tuple - -_marker = object() - -class XHRPredicate(object): - def __init__(self, val, config): - self.val = bool(val) - - def text(self): - return 'xhr = %s' % self.val - - phash = text - - def __call__(self, context, request): - return bool(request.is_xhr) is self.val - -class RequestMethodPredicate(object): - def __init__(self, val, config): - request_method = as_sorted_tuple(val) - if 'GET' in request_method and 'HEAD' not in request_method: - # GET implies HEAD too - request_method = as_sorted_tuple(request_method + ('HEAD',)) - self.val = request_method - - def text(self): - return 'request_method = %s' % (','.join(self.val)) - - phash = text - - def __call__(self, context, request): - return request.method in self.val - -class PathInfoPredicate(object): - def __init__(self, val, config): - self.orig = val - try: - val = re.compile(val) - except re.error as why: - raise ConfigurationError(why.args[0]) - self.val = val - - def text(self): - return 'path_info = %s' % (self.orig,) - - phash = text - - def __call__(self, context, request): - return self.val.match(request.upath_info) is not None - -class RequestParamPredicate(object): - def __init__(self, val, config): - val = as_sorted_tuple(val) - reqs = [] - for p in val: - k = p - v = None - if p.startswith('='): - if '=' in p[1:]: - k, v = p[1:].split('=', 1) - k = '=' + k - k, v = k.strip(), v.strip() - elif '=' in p: - k, v = p.split('=', 1) - k, v = k.strip(), v.strip() - reqs.append((k, v)) - self.val = val - self.reqs = reqs - - def text(self): - return 'request_param %s' % ','.join( - ['%s=%s' % (x,y) if y else x for x, y in self.reqs] - ) - - phash = text - - def __call__(self, context, request): - for k, v in self.reqs: - actual = request.params.get(k) - if actual is None: - return False - if v is not None and actual != v: - return False - return True - -class HeaderPredicate(object): - def __init__(self, val, config): - name = val - v = None - if ':' in name: - name, val_str = name.split(':', 1) - try: - v = re.compile(val_str) - except re.error as why: - raise ConfigurationError(why.args[0]) - if v is None: - self._text = 'header %s' % (name,) - else: - self._text = 'header %s=%s' % (name, val_str) - self.name = name - self.val = v - - def text(self): - return self._text - - phash = text - - def __call__(self, context, request): - if self.val is None: - return self.name in request.headers - val = request.headers.get(self.name) - if val is None: - return False - return self.val.match(val) is not None - -class AcceptPredicate(object): - def __init__(self, val, config): - self.val = val - - def text(self): - return 'accept = %s' % (self.val,) - - phash = text - - def __call__(self, context, request): - return self.val in request.accept - -class ContainmentPredicate(object): - def __init__(self, val, config): - self.val = config.maybe_dotted(val) - - def text(self): - return 'containment = %s' % (self.val,) - - phash = text - - def __call__(self, context, request): - ctx = getattr(request, 'context', context) - return find_interface(ctx, self.val) is not None - -class RequestTypePredicate(object): - def __init__(self, val, config): - self.val = val - - def text(self): - return 'request_type = %s' % (self.val,) - - phash = text - - def __call__(self, context, request): - return self.val.providedBy(request) - -class MatchParamPredicate(object): - def __init__(self, val, config): - val = as_sorted_tuple(val) - self.val = val - reqs = [ p.split('=', 1) for p in val ] - self.reqs = [ (x.strip(), y.strip()) for x, y in reqs ] - - def text(self): - return 'match_param %s' % ','.join( - ['%s=%s' % (x,y) for x, y in self.reqs] - ) - - phash = text - - def __call__(self, context, request): - if not request.matchdict: - # might be None - return False - for k, v in self.reqs: - if request.matchdict.get(k) != v: - return False - return True - -class CustomPredicate(object): - def __init__(self, func, config): - self.func = func - - def text(self): - return getattr( - self.func, - '__text__', - 'custom predicate: %s' % object_description(self.func) - ) - - def phash(self): - # using hash() here rather than id() is intentional: we - # want to allow custom predicates that are part of - # frameworks to be able to define custom __hash__ - # functions for custom predicates, so that the hash output - # of predicate instances which are "logically the same" - # may compare equal. - return 'custom:%r' % hash(self.func) - - def __call__(self, context, request): - return self.func(context, request) - - -class TraversePredicate(object): - # Can only be used as a *route* "predicate"; it adds 'traverse' to the - # matchdict if it's specified in the routing args. This causes the - # ResourceTreeTraverser to use the resolved traverse pattern as the - # traversal path. - def __init__(self, val, config): - _, self.tgenerate = _compile_route(val) - self.val = val - - def text(self): - return 'traverse matchdict pseudo-predicate' - - def phash(self): - # This isn't actually a predicate, it's just a infodict modifier that - # injects ``traverse`` into the matchdict. As a result, we don't - # need to update the hash. - return '' - - def __call__(self, context, request): - if 'traverse' in context: - return True - m = context['match'] - tvalue = self.tgenerate(m) # tvalue will be urlquoted string - m['traverse'] = traversal_path(tvalue) - # This isn't actually a predicate, it's just a infodict modifier that - # injects ``traverse`` into the matchdict. As a result, we just - # return True. - return True - -class CheckCSRFTokenPredicate(object): - - check_csrf_token = staticmethod(check_csrf_token) # testing - - def __init__(self, val, config): - self.val = val - - def text(self): - return 'check_csrf = %s' % (self.val,) - - phash = text - - def __call__(self, context, request): - val = self.val - if val: - if val is True: - val = 'csrf_token' - return self.check_csrf_token(request, val, raises=False) - return True - -class PhysicalPathPredicate(object): - def __init__(self, val, config): - if is_nonstr_iter(val): - self.val = tuple(val) - else: - val = tuple(filter(None, val.split('/'))) - self.val = ('',) + val - - def text(self): - return 'physical_path = %s' % (self.val,) - - phash = text - - def __call__(self, context, request): - if getattr(context, '__name__', _marker) is not _marker: - return resource_path_tuple(context) == self.val - return False - -class EffectivePrincipalsPredicate(object): - def __init__(self, val, config): - if is_nonstr_iter(val): - self.val = set(val) - else: - self.val = set((val,)) - - def text(self): - return 'effective_principals = %s' % sorted(list(self.val)) - - phash = text - - def __call__(self, context, request): - req_principals = request.effective_principals - if is_nonstr_iter(req_principals): - rpset = set(req_principals) - if self.val.issubset(rpset): - return True - return False - +import zope.deprecation +zope.deprecation.moved('pyramid.predicates', 'Pyramid 2.0') diff --git a/pyramid/config/routes.py b/pyramid/config/routes.py index 90d4d47d2..203baa128 100644 --- a/pyramid/config/routes.py +++ b/pyramid/config/routes.py @@ -13,12 +13,10 @@ from pyramid.registry import predvalseq from pyramid.request import route_request_iface from pyramid.urldispatch import RoutesMapper -from pyramid.config.util import ( - action_method, - as_sorted_tuple, - ) +from pyramid.config.util import action_method +from pyramid.util import as_sorted_tuple -import pyramid.config.predicates +import pyramid.predicates class RoutesConfiguratorMixin(object): @action_method @@ -446,7 +444,7 @@ class RoutesConfiguratorMixin(object): ) def add_default_route_predicates(self): - p = pyramid.config.predicates + p = pyramid.predicates for (name, factory) in ( ('xhr', p.XHRPredicate), ('request_method', p.RequestMethodPredicate), diff --git a/pyramid/config/security.py b/pyramid/config/security.py index 02732c042..33593376b 100644 --- a/pyramid/config/security.py +++ b/pyramid/config/security.py @@ -9,9 +9,9 @@ from pyramid.interfaces import ( PHASE2_CONFIG, ) -from pyramid.config.util import as_sorted_tuple from pyramid.exceptions import ConfigurationError from pyramid.util import action_method +from pyramid.util import as_sorted_tuple class SecurityConfiguratorMixin(object): @action_method diff --git a/pyramid/config/tweens.py b/pyramid/config/tweens.py index 0aeb01fe3..16712ab16 100644 --- a/pyramid/config/tweens.py +++ b/pyramid/config/tweens.py @@ -17,9 +17,9 @@ from pyramid.tweens import ( from pyramid.config.util import ( action_method, - is_string_or_iterable, TopologicalSorter, ) +from pyramid.util import is_string_or_iterable class TweensConfiguratorMixin(object): def add_tween(self, tween_factory, under=None, over=None): diff --git a/pyramid/config/util.py b/pyramid/config/util.py index 626e8d5fe..67bba9593 100644 --- a/pyramid/config/util.py +++ b/pyramid/config/util.py @@ -4,8 +4,7 @@ import inspect from pyramid.compat import ( bytes_, getargspec, - is_nonstr_iter, - string_types, + is_nonstr_iter ) from pyramid.compat import im_func @@ -24,18 +23,6 @@ ActionInfo = ActionInfo # support bw compat imports MAX_ORDER = 1 << 30 DEFAULT_PHASH = md5().hexdigest() -def is_string_or_iterable(v): - if isinstance(v, string_types): - return True - if hasattr(v, '__iter__'): - return True - -def as_sorted_tuple(val): - if not is_nonstr_iter(val): - val = (val,) - val = tuple(sorted(val)) - return val - class not_(object): """ diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 6082d8b48..65c9da585 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -70,10 +70,11 @@ import pyramid.util from pyramid.util import ( viewdefaults, action_method, + as_sorted_tuple, TopologicalSorter, ) -import pyramid.config.predicates +import pyramid.predicates import pyramid.viewderivers from pyramid.viewderivers import ( @@ -89,7 +90,6 @@ from pyramid.viewderivers import ( from pyramid.config.util import ( DEFAULT_PHASH, MAX_ORDER, - as_sorted_tuple, ) urljoin = urlparse.urljoin @@ -1143,7 +1143,7 @@ class ViewsConfiguratorMixin(object): ) def add_default_view_predicates(self): - p = pyramid.config.predicates + p = pyramid.predicates for (name, factory) in ( ('xhr', p.XHRPredicate), ('request_method', p.RequestMethodPredicate), diff --git a/pyramid/predicates.py b/pyramid/predicates.py new file mode 100644 index 000000000..7c3a778ca --- /dev/null +++ b/pyramid/predicates.py @@ -0,0 +1,300 @@ +import re + +from pyramid.exceptions import ConfigurationError + +from pyramid.compat import is_nonstr_iter + +from pyramid.session import check_csrf_token +from pyramid.traversal import ( + find_interface, + traversal_path, + resource_path_tuple + ) + +from pyramid.urldispatch import _compile_route +from pyramid.util import object_description +from pyramid.util import as_sorted_tuple + +_marker = object() + +class XHRPredicate(object): + def __init__(self, val, config): + self.val = bool(val) + + def text(self): + return 'xhr = %s' % self.val + + phash = text + + def __call__(self, context, request): + return bool(request.is_xhr) is self.val + +class RequestMethodPredicate(object): + def __init__(self, val, config): + request_method = as_sorted_tuple(val) + if 'GET' in request_method and 'HEAD' not in request_method: + # GET implies HEAD too + request_method = as_sorted_tuple(request_method + ('HEAD',)) + self.val = request_method + + def text(self): + return 'request_method = %s' % (','.join(self.val)) + + phash = text + + def __call__(self, context, request): + return request.method in self.val + +class PathInfoPredicate(object): + def __init__(self, val, config): + self.orig = val + try: + val = re.compile(val) + except re.error as why: + raise ConfigurationError(why.args[0]) + self.val = val + + def text(self): + return 'path_info = %s' % (self.orig,) + + phash = text + + def __call__(self, context, request): + return self.val.match(request.upath_info) is not None + +class RequestParamPredicate(object): + def __init__(self, val, config): + val = as_sorted_tuple(val) + reqs = [] + for p in val: + k = p + v = None + if p.startswith('='): + if '=' in p[1:]: + k, v = p[1:].split('=', 1) + k = '=' + k + k, v = k.strip(), v.strip() + elif '=' in p: + k, v = p.split('=', 1) + k, v = k.strip(), v.strip() + reqs.append((k, v)) + self.val = val + self.reqs = reqs + + def text(self): + return 'request_param %s' % ','.join( + ['%s=%s' % (x,y) if y else x for x, y in self.reqs] + ) + + phash = text + + def __call__(self, context, request): + for k, v in self.reqs: + actual = request.params.get(k) + if actual is None: + return False + if v is not None and actual != v: + return False + return True + +class HeaderPredicate(object): + def __init__(self, val, config): + name = val + v = None + if ':' in name: + name, val_str = name.split(':', 1) + try: + v = re.compile(val_str) + except re.error as why: + raise ConfigurationError(why.args[0]) + if v is None: + self._text = 'header %s' % (name,) + else: + self._text = 'header %s=%s' % (name, val_str) + self.name = name + self.val = v + + def text(self): + return self._text + + phash = text + + def __call__(self, context, request): + if self.val is None: + return self.name in request.headers + val = request.headers.get(self.name) + if val is None: + return False + return self.val.match(val) is not None + +class AcceptPredicate(object): + def __init__(self, val, config): + self.val = val + + def text(self): + return 'accept = %s' % (self.val,) + + phash = text + + def __call__(self, context, request): + return self.val in request.accept + +class ContainmentPredicate(object): + def __init__(self, val, config): + self.val = config.maybe_dotted(val) + + def text(self): + return 'containment = %s' % (self.val,) + + phash = text + + def __call__(self, context, request): + ctx = getattr(request, 'context', context) + return find_interface(ctx, self.val) is not None + +class RequestTypePredicate(object): + def __init__(self, val, config): + self.val = val + + def text(self): + return 'request_type = %s' % (self.val,) + + phash = text + + def __call__(self, context, request): + return self.val.providedBy(request) + +class MatchParamPredicate(object): + def __init__(self, val, config): + val = as_sorted_tuple(val) + self.val = val + reqs = [ p.split('=', 1) for p in val ] + self.reqs = [ (x.strip(), y.strip()) for x, y in reqs ] + + def text(self): + return 'match_param %s' % ','.join( + ['%s=%s' % (x,y) for x, y in self.reqs] + ) + + phash = text + + def __call__(self, context, request): + if not request.matchdict: + # might be None + return False + for k, v in self.reqs: + if request.matchdict.get(k) != v: + return False + return True + +class CustomPredicate(object): + def __init__(self, func, config): + self.func = func + + def text(self): + return getattr( + self.func, + '__text__', + 'custom predicate: %s' % object_description(self.func) + ) + + def phash(self): + # using hash() here rather than id() is intentional: we + # want to allow custom predicates that are part of + # frameworks to be able to define custom __hash__ + # functions for custom predicates, so that the hash output + # of predicate instances which are "logically the same" + # may compare equal. + return 'custom:%r' % hash(self.func) + + def __call__(self, context, request): + return self.func(context, request) + + +class TraversePredicate(object): + # Can only be used as a *route* "predicate"; it adds 'traverse' to the + # matchdict if it's specified in the routing args. This causes the + # ResourceTreeTraverser to use the resolved traverse pattern as the + # traversal path. + def __init__(self, val, config): + _, self.tgenerate = _compile_route(val) + self.val = val + + def text(self): + return 'traverse matchdict pseudo-predicate' + + def phash(self): + # This isn't actually a predicate, it's just a infodict modifier that + # injects ``traverse`` into the matchdict. As a result, we don't + # need to update the hash. + return '' + + def __call__(self, context, request): + if 'traverse' in context: + return True + m = context['match'] + tvalue = self.tgenerate(m) # tvalue will be urlquoted string + m['traverse'] = traversal_path(tvalue) + # This isn't actually a predicate, it's just a infodict modifier that + # injects ``traverse`` into the matchdict. As a result, we just + # return True. + return True + +class CheckCSRFTokenPredicate(object): + + check_csrf_token = staticmethod(check_csrf_token) # testing + + def __init__(self, val, config): + self.val = val + + def text(self): + return 'check_csrf = %s' % (self.val,) + + phash = text + + def __call__(self, context, request): + val = self.val + if val: + if val is True: + val = 'csrf_token' + return self.check_csrf_token(request, val, raises=False) + return True + +class PhysicalPathPredicate(object): + def __init__(self, val, config): + if is_nonstr_iter(val): + self.val = tuple(val) + else: + val = tuple(filter(None, val.split('/'))) + self.val = ('',) + val + + def text(self): + return 'physical_path = %s' % (self.val,) + + phash = text + + def __call__(self, context, request): + if getattr(context, '__name__', _marker) is not _marker: + return resource_path_tuple(context) == self.val + return False + +class EffectivePrincipalsPredicate(object): + def __init__(self, val, config): + if is_nonstr_iter(val): + self.val = set(val) + else: + self.val = set((val,)) + + def text(self): + return 'effective_principals = %s' % sorted(list(self.val)) + + phash = text + + def __call__(self, context, request): + req_principals = request.effective_principals + if is_nonstr_iter(req_principals): + rpset = set(req_principals) + if self.val.issubset(rpset): + return True + return False + diff --git a/pyramid/tests/test_config/test_util.py b/pyramid/tests/test_config/test_util.py index ccf7fa260..398b6fba8 100644 --- a/pyramid/tests/test_config/test_util.py +++ b/pyramid/tests/test_config/test_util.py @@ -5,7 +5,7 @@ class TestPredicateList(unittest.TestCase): def _makeOne(self): from pyramid.config.util import PredicateList - from pyramid.config import predicates + from pyramid import predicates inst = PredicateList() for name, factory in ( ('xhr', predicates.XHRPredicate), @@ -594,6 +594,15 @@ class TestNotted(unittest.TestCase): self.assertEqual(inst.phash(), '') self.assertEqual(inst(None, None), True) + +class TestDeprecatedPredicates(unittest.TestCase): + def test_it(self): + import warnings + with warnings.catch_warnings(record=True) as w: + warnings.filterwarnings('always') + from pyramid.config.predicates import XHRPredicate + self.assertEqual(len(w), 1) + class DummyPredicate(object): def __init__(self, result): self.result = result diff --git a/pyramid/tests/test_config/test_predicates.py b/pyramid/tests/test_predicates.py index 9cd8f2734..8a002c24e 100644 --- a/pyramid/tests/test_config/test_predicates.py +++ b/pyramid/tests/test_predicates.py @@ -6,7 +6,7 @@ from pyramid.compat import text_ class TestXHRPredicate(unittest.TestCase): def _makeOne(self, val): - from pyramid.config.predicates import XHRPredicate + from pyramid.predicates import XHRPredicate return XHRPredicate(val, None) def test___call___true(self): @@ -33,7 +33,7 @@ class TestXHRPredicate(unittest.TestCase): class TestRequestMethodPredicate(unittest.TestCase): def _makeOne(self, val): - from pyramid.config.predicates import RequestMethodPredicate + from pyramid.predicates import RequestMethodPredicate return RequestMethodPredicate(val, None) def test_ctor_get_but_no_head(self): @@ -71,7 +71,7 @@ class TestRequestMethodPredicate(unittest.TestCase): class TestPathInfoPredicate(unittest.TestCase): def _makeOne(self, val): - from pyramid.config.predicates import PathInfoPredicate + from pyramid.predicates import PathInfoPredicate return PathInfoPredicate(val, None) def test_ctor_compilefail(self): @@ -102,7 +102,7 @@ class TestPathInfoPredicate(unittest.TestCase): class TestRequestParamPredicate(unittest.TestCase): def _makeOne(self, val): - from pyramid.config.predicates import RequestParamPredicate + from pyramid.predicates import RequestParamPredicate return RequestParamPredicate(val, None) def test___call___true_exists(self): @@ -174,7 +174,7 @@ class TestRequestParamPredicate(unittest.TestCase): class TestMatchParamPredicate(unittest.TestCase): def _makeOne(self, val): - from pyramid.config.predicates import MatchParamPredicate + from pyramid.predicates import MatchParamPredicate return MatchParamPredicate(val, None) def test___call___true_single(self): @@ -216,7 +216,7 @@ class TestMatchParamPredicate(unittest.TestCase): class TestCustomPredicate(unittest.TestCase): def _makeOne(self, val): - from pyramid.config.predicates import CustomPredicate + from pyramid.predicates import CustomPredicate return CustomPredicate(val, None) def test___call___true(self): @@ -255,7 +255,7 @@ class TestCustomPredicate(unittest.TestCase): class TestTraversePredicate(unittest.TestCase): def _makeOne(self, val): - from pyramid.config.predicates import TraversePredicate + from pyramid.predicates import TraversePredicate return TraversePredicate(val, None) def test___call__traverse_has_remainder_already(self): @@ -297,7 +297,7 @@ class TestTraversePredicate(unittest.TestCase): class Test_CheckCSRFTokenPredicate(unittest.TestCase): def _makeOne(self, val, config): - from pyramid.config.predicates import CheckCSRFTokenPredicate + from pyramid.predicates import CheckCSRFTokenPredicate return CheckCSRFTokenPredicate(val, config) def test_text(self): @@ -340,7 +340,7 @@ class Test_CheckCSRFTokenPredicate(unittest.TestCase): class TestHeaderPredicate(unittest.TestCase): def _makeOne(self, val): - from pyramid.config.predicates import HeaderPredicate + from pyramid.predicates import HeaderPredicate return HeaderPredicate(val, None) def test___call___true_exists(self): @@ -404,7 +404,7 @@ class TestHeaderPredicate(unittest.TestCase): class Test_PhysicalPathPredicate(unittest.TestCase): def _makeOne(self, val, config): - from pyramid.config.predicates import PhysicalPathPredicate + from pyramid.predicates import PhysicalPathPredicate return PhysicalPathPredicate(val, config) def test_text(self): @@ -468,7 +468,7 @@ class Test_EffectivePrincipalsPredicate(unittest.TestCase): testing.tearDown() def _makeOne(self, val, config): - from pyramid.config.predicates import EffectivePrincipalsPredicate + from pyramid.predicates import EffectivePrincipalsPredicate return EffectivePrincipalsPredicate(val, config) def test_text(self): diff --git a/pyramid/util.py b/pyramid/util.py index d5b3c6d72..3337d410d 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -28,13 +28,24 @@ from pyramid.compat import ( from pyramid.interfaces import IActionInfo from pyramid.path import DottedNameResolver as _DottedNameResolver +_marker = object() + class DottedNameResolver(_DottedNameResolver): def __init__(self, package=None): # default to package = None for bw compat _DottedNameResolver.__init__(self, package) -_marker = object() - +def is_string_or_iterable(v): + if isinstance(v, string_types): + return True + if hasattr(v, '__iter__'): + return True + +def as_sorted_tuple(val): + if not is_nonstr_iter(val): + val = (val,) + val = tuple(sorted(val)) + return val class InstancePropertyHelper(object): """A helper object for assigning properties and descriptors to instances. |
