From 9c8ec5c7b7f12abb741f9d4467bc85f15b893420 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 5 Aug 2012 17:28:01 -0400 Subject: use a predicate list for routes, introduce the concept of deferred discriminators, change conflict resolution to deal with deferred discriminators, make predicates take a config argument, get rid of legacy make_predicates function --- pyramid/config/__init__.py | 159 ++++++++----- pyramid/config/predicates.py | 50 ++-- pyramid/config/routes.py | 101 +++++++-- pyramid/config/util.py | 328 ++++++--------------------- pyramid/config/views.py | 208 ++++++++++------- pyramid/testing.py | 2 + pyramid/tests/test_config/test_init.py | 41 ++-- pyramid/tests/test_config/test_predicates.py | 18 +- pyramid/tests/test_config/test_routes.py | 2 +- pyramid/tests/test_config/test_util.py | 95 +++++--- pyramid/tests/test_config/test_views.py | 9 +- pyramid/tests/test_testing.py | 3 + 12 files changed, 526 insertions(+), 490 deletions(-) diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index 5eb860ed5..2fca7a162 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -1,4 +1,5 @@ import inspect +import itertools import logging import operator import os @@ -71,6 +72,7 @@ from pyramid.config.tweens import TweensConfiguratorMixin from pyramid.config.util import ( action_method, ActionInfo, + Deferred, ) from pyramid.config.views import ViewsConfiguratorMixin from pyramid.config.zca import ZCAConfiguratorMixin @@ -354,6 +356,7 @@ class Configurator( self.add_renderer(name, renderer) self.add_default_view_predicates() + self.add_default_route_predicates() if exceptionresponse_view is not None: exceptionresponse_view = self.maybe_dotted(exceptionresponse_view) @@ -549,6 +552,10 @@ class Configurator( introspectables = () if autocommit: + if isinstance(discriminator, Deferred): + # callables can depend on the side effects of resolving a + # deferred discriminator + discriminator.resolve() if callable is not None: callable(*args, **kw) for introspectable in introspectables: @@ -1060,6 +1067,11 @@ class ActionState(object): if clear: del self.actions[:] +def undefer(v): + if isinstance(v, Deferred): + v = v.resolve() + return v + # this function is licensed under the ZPL (stolen from Zope) def resolveConflicts(actions): """Resolve conflicting actions @@ -1072,73 +1084,94 @@ def resolveConflicts(actions): other conflicting actions. """ - # organize actions by discriminators - unique = {} - output = [] - for i, action in enumerate(actions): - if not isinstance(action, dict): + def orderandpos((n, v)): + if not isinstance(v, dict): # old-style tuple action - action = expand_action(*action) + v = expand_action(*v) + return (v['order'] or 0, n) + + sactions = sorted(enumerate(actions), key=orderandpos) + def orderonly((n,v)): + if not isinstance(v, dict): + # old-style tuple action + v = expand_action(*v) + return v['order'] or 0 + + for order, actiongroup in itertools.groupby(sactions, orderonly): # "order" is an integer grouping. Actions in a lower order will be - # executed before actions in a higher order. Within an order, - # actions are executed sequentially based on original action ordering - # ("i"). - order = action['order'] or 0 - discriminator = action['discriminator'] - - # "ainfo" is a tuple of (order, i, action) where "order" is a - # user-supplied grouping, "i" is an integer expressing the relative - # position of this action in the action list being resolved, and - # "action" is an action dictionary. The purpose of an ainfo is to - # associate an "order" and an "i" with a particular action; "order" - # and "i" exist for sorting purposes after conflict resolution. - ainfo = (order, i, action) - - if discriminator is None: - # The discriminator is None, so this action can never conflict. - # We can add it directly to the result. + # executed before actions in a higher order. All of the actions in + # one grouping will be executed (its callable, if any will be called) + # before any of the actions in the next. + + unique = {} + output = [] + + for i, action in actiongroup: + # Within an order, actions are executed sequentially based on + # original action ordering ("i"). + + if not isinstance(action, dict): + # old-style tuple action + action = expand_action(*action) + + # "ainfo" is a tuple of (order, i, action) where "order" is a + # user-supplied grouping, "i" is an integer expressing the relative + # position of this action in the action list being resolved, and + # "action" is an action dictionary. The purpose of an ainfo is to + # associate an "order" and an "i" with a particular action; "order" + # and "i" exist for sorting purposes after conflict resolution. + ainfo = (order, i, action) + + discriminator = undefer(action['discriminator']) + action['discriminator'] = discriminator + + if discriminator is None: + # The discriminator is None, so this action can never conflict. + # We can add it directly to the result. + output.append(ainfo) + continue + + L = unique.setdefault(discriminator, []) + L.append(ainfo) + + # Check for conflicts + conflicts = {} + + for discriminator, ainfos in unique.items(): + # We use (includepath, order, i) as a sort key because we need to + # sort the actions by the paths so that the shortest path with a + # given prefix comes first. The "first" action is the one with the + # shortest include path. We break sorting ties using "order", then + # "i". + def bypath(ainfo): + path, order, i = ainfo[2]['includepath'], ainfo[0], ainfo[1] + return path, order, i + + ainfos.sort(key=bypath) + ainfo, rest = ainfos[0], ainfos[1:] output.append(ainfo) - continue - - L = unique.setdefault(discriminator, []) - L.append(ainfo) - - # Check for conflicts - conflicts = {} - - for discriminator, ainfos in unique.items(): - - # We use (includepath, order, i) as a sort key because we need to - # sort the actions by the paths so that the shortest path with a - # given prefix comes first. The "first" action is the one with the - # shortest include path. We break sorting ties using "order", then - # "i". - def bypath(ainfo): - path, order, i = ainfo[2]['includepath'], ainfo[0], ainfo[1] - return path, order, i - - ainfos.sort(key=bypath) - ainfo, rest = ainfos[0], ainfos[1:] - output.append(ainfo) - _, _, action = ainfo - basepath, baseinfo, discriminator = (action['includepath'], - action['info'], - action['discriminator']) - - for _, _, action in rest: - includepath = action['includepath'] - # Test whether path is a prefix of opath - if (includepath[:len(basepath)] != basepath # not a prefix - or includepath == basepath): - L = conflicts.setdefault(discriminator, [baseinfo]) - L.append(action['info']) - - if conflicts: - raise ConfigurationConflictError(conflicts) - - # sort conflict-resolved actions by (order, i) and return them - return [ x[2] for x in sorted(output, key=operator.itemgetter(0, 1))] + _, _, action = ainfo + basepath, baseinfo, discriminator = ( + action['includepath'], + action['info'], + action['discriminator'], + ) + + for _, _, action in rest: + includepath = action['includepath'] + # Test whether path is a prefix of opath + if (includepath[:len(basepath)] != basepath # not a prefix + or includepath == basepath): + L = conflicts.setdefault(discriminator, [baseinfo]) + L.append(action['info']) + + if conflicts: + raise ConfigurationConflictError(conflicts) + + # sort conflict-resolved actions by (order, i) and yield them one by one + for a in [x[2] for x in sorted(output, key=operator.itemgetter(0, 1))]: + yield a def expand_action(discriminator, callable=None, args=(), kw=None, includepath=(), info=None, order=0, introspectables=()): diff --git a/pyramid/config/predicates.py b/pyramid/config/predicates.py index 37f75462d..311d47860 100644 --- a/pyramid/config/predicates.py +++ b/pyramid/config/predicates.py @@ -11,10 +11,12 @@ from pyramid.traversal import ( from pyramid.urldispatch import _compile_route +from pyramid.util import object_description + from .util import as_sorted_tuple class XHRPredicate(object): - def __init__(self, val): + def __init__(self, val, config): self.val = bool(val) def text(self): @@ -26,7 +28,7 @@ class XHRPredicate(object): return bool(request.is_xhr) is self.val class RequestMethodPredicate(object): - def __init__(self, val): + def __init__(self, val, config): self.val = as_sorted_tuple(val) def text(self): @@ -38,7 +40,7 @@ class RequestMethodPredicate(object): return request.method in self.val class PathInfoPredicate(object): - def __init__(self, val): + def __init__(self, val, config): self.orig = val try: val = re.compile(val) @@ -55,7 +57,7 @@ class PathInfoPredicate(object): return self.val.match(request.upath_info) is not None class RequestParamPredicate(object): - def __init__(self, val): + def __init__(self, val, config): name = val v = None if '=' in name: @@ -80,7 +82,7 @@ class RequestParamPredicate(object): class HeaderPredicate(object): - def __init__(self, val): + def __init__(self, val, config): name = val v = None if ':' in name: @@ -110,7 +112,7 @@ class HeaderPredicate(object): return self.val.match(val) is not None class AcceptPredicate(object): - def __init__(self, val): + def __init__(self, val, config): self.val = val def text(self): @@ -122,8 +124,8 @@ class AcceptPredicate(object): return self.val in request.accept class ContainmentPredicate(object): - def __init__(self, val): - self.val = val + def __init__(self, val, config): + self.val = config.maybe_dotted(val) def text(self): return 'containment = %s' % (self.val,) @@ -135,7 +137,7 @@ class ContainmentPredicate(object): return find_interface(ctx, self.val) is not None class RequestTypePredicate(object): - def __init__(self, val): + def __init__(self, val, config): self.val = val def text(self): @@ -147,7 +149,7 @@ class RequestTypePredicate(object): return self.val.providedBy(request) class MatchParamPredicate(object): - def __init__(self, val): + def __init__(self, val, config): if not is_nonstr_iter(val): val = (val,) val = sorted(val) @@ -169,13 +171,23 @@ class MatchParamPredicate(object): return True class CustomPredicate(object): - def __init__(self, func): + def __init__(self, func, config): self.func = func def text(self): - return getattr(self.func, '__text__', repr(self.func)) + 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): @@ -183,7 +195,11 @@ class CustomPredicate(object): class TraversePredicate(object): - def __init__(self, val): + # 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 @@ -191,12 +207,18 @@ class TraversePredicate(object): 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 = 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 diff --git a/pyramid/config/routes.py b/pyramid/config/routes.py index ea39b6805..ff285569d 100644 --- a/pyramid/config/routes.py +++ b/pyramid/config/routes.py @@ -1,9 +1,11 @@ import warnings from pyramid.interfaces import ( + IPredicateList, IRequest, IRouteRequest, IRoutesMapper, + PHASE1_CONFIG, PHASE2_CONFIG, ) @@ -13,10 +15,13 @@ from pyramid.urldispatch import RoutesMapper from pyramid.config.util import ( action_method, - make_predicates, as_sorted_tuple, + PredicateList, + predvalseq, ) +from pyramid.config import predicates + class RoutesConfiguratorMixin(object): @action_method def add_route(self, @@ -28,7 +33,7 @@ class RoutesConfiguratorMixin(object): factory=None, for_=None, header=None, - xhr=False, + xhr=None, accept=None, path_info=None, request_method=None, @@ -44,7 +49,7 @@ class RoutesConfiguratorMixin(object): path=None, pregenerator=None, static=False, - ): + **other_predicates): """ Add a :term:`route configuration` to the current configuration state, as well as possibly a :term:`view configuration` to be used to specify a :term:`view callable` @@ -254,6 +259,14 @@ class RoutesConfiguratorMixin(object): :ref:`custom_route_predicates` for more information about ``info``. + other_predicates + + Pass a key/value pair here to use a third-party predicate registered + via :meth:`pyramid.config.Configurator.add_view_predicate`. More + than one key/value pair can be used at the same time. See + :ref:`registering_thirdparty_predicates` for more information + about third-party predicates. + View-Related Arguments .. warning:: @@ -351,17 +364,6 @@ class RoutesConfiguratorMixin(object): if request_method is not None: request_method = as_sorted_tuple(request_method) - ignored, predicates, ignored = make_predicates( - xhr=xhr, - request_method=request_method, - path_info=path_info, - request_param=request_param, - header=header, - accept=accept, - traverse=traverse, - custom=custom_predicates - ) - factory = self.maybe_dotted(factory) if pattern is None: pattern = path @@ -417,8 +419,24 @@ class RoutesConfiguratorMixin(object): request_iface, IRouteRequest, name=name) def register_connect(): + pvals = other_predicates + pvals.update( + dict( + xhr=xhr, + request_method=request_method, + path_info=path_info, + request_param=request_param, + header=header, + accept=accept, + traverse=traverse, + custom=predvalseq(custom_predicates), + ) + ) + + predlist = self.route_predlist + _, preds, _ = predlist.make(self, **pvals) route = mapper.connect( - name, pattern, factory, predicates=predicates, + name, pattern, factory, predicates=preds, pregenerator=pregenerator, static=static ) intr['object'] = route @@ -447,6 +465,59 @@ class RoutesConfiguratorMixin(object): attr=view_attr, ) + @property + def route_predlist(self): + predlist = self.registry.queryUtility(IPredicateList, name='route') + if predlist is None: + predlist = PredicateList() + self.registry.registerUtility(predlist, IPredicateList, + name='route') + return predlist + + @action_method + def add_route_predicate(self, name, factory, weighs_more_than=None, + weighs_less_than=None): + """ Adds a route predicate factory. The view predicate can later be + named as a keyword argument to + :meth:`pyramid.config.Configurator.add_route`. + + ``name`` should be the name of the predicate. It must be a valid + Python identifier (it will be used as a keyword argument to + ``add_view``). + + ``factory`` should be a :term:`predicate factory`. + """ + discriminator = ('route predicate', name) + intr = self.introspectable( + 'route predicates', + discriminator, + 'route predicate named %s' % name, + 'route predicate') + intr['name'] = name + intr['factory'] = factory + intr['weighs_more_than'] = weighs_more_than + intr['weighs_less_than'] = weighs_less_than + def register(): + predlist = self.route_predlist + predlist.add(name, factory, weighs_more_than=weighs_more_than, + weighs_less_than=weighs_less_than) + # must be registered before routes connected + self.action(discriminator, register, introspectables=(intr,), + order=PHASE1_CONFIG) + + def add_default_route_predicates(self): + for (name, factory) in ( + ('xhr', predicates.XHRPredicate), + ('request_method', predicates.RequestMethodPredicate), + ('path_info', predicates.PathInfoPredicate), + ('request_param', predicates.RequestParamPredicate), + ('header', predicates.HeaderPredicate), + ('accept', predicates.AcceptPredicate), + ('custom', predicates.CustomPredicate), + ('traverse', predicates.TraversePredicate), + ): + self.add_route_predicate(name, factory) + def get_routes_mapper(self): """ Return the :term:`routes mapper` object associated with this configurator's :term:`registry`.""" diff --git a/pyramid/config/util.py b/pyramid/config/util.py index da3766deb..ce4a4a728 100644 --- a/pyramid/config/util.py +++ b/pyramid/config/util.py @@ -1,4 +1,3 @@ -import re import traceback from zope.interface import implementer @@ -12,11 +11,6 @@ from pyramid.compat import ( from pyramid.exceptions import ConfigurationError -from pyramid.traversal import ( - find_interface, - traversal_path, - ) - from hashlib import md5 MAX_ORDER = 1 << 30 @@ -64,236 +58,6 @@ def action_method(wrapped): wrapper.__docobj__ = wrapped # for sphinx return wrapper -def make_predicates(xhr=None, request_method=None, path_info=None, - request_param=None, match_param=None, header=None, - accept=None, containment=None, request_type=None, - traverse=None, custom=()): - - # PREDICATES - # ---------- - # - # Given an argument list, a predicate list is computed. - # Predicates are added to a predicate list in (presumed) - # computation expense order. All predicates associated with a - # view or route must evaluate true for the view or route to - # "match" during a request. Elsewhere in the code, we evaluate - # predicates using a generator expression. The fastest predicate - # should be evaluated first, then the next fastest, and so on, as - # if one returns false, the remainder of the predicates won't need - # to be evaluated. - # - # While we compute predicates, we also compute a predicate hash - # (aka phash) that can be used by a caller to identify identical - # predicate lists. - # - # ORDERING - # -------- - # - # A "order" is computed for the predicate list. An order is - # a scoring. - # - # Each predicate is associated with a weight value, which is a - # multiple of 2. The weight of a predicate symbolizes the - # relative potential "importance" of the predicate to all other - # predicates. A larger weight indicates greater importance. - # - # All weights for a given predicate list are bitwise ORed together - # to create a "score"; this score is then subtracted from - # MAX_ORDER and divided by an integer representing the number of - # predicates+1 to determine the order. - # - # The order represents the ordering in which a "multiview" ( a - # collection of views that share the same context/request/name - # triad but differ in other ways via predicates) will attempt to - # call its set of views. Views with lower orders will be tried - # first. The intent is to a) ensure that views with more - # predicates are always evaluated before views with fewer - # predicates and b) to ensure a stable call ordering of views that - # share the same number of predicates. Views which do not have - # any predicates get an order of MAX_ORDER, meaning that they will - # be tried very last. - - # NB: each predicate callable constructed by this function (or examined - # by this function, in the case of custom predicates) must leave this - # function with a ``__text__`` attribute. The subsystem which reports - # errors when no predicates match depends upon the existence of this - # attribute on each predicate callable. - - predicates = [] - weights = [] - h = md5() - - if xhr: - def xhr_predicate(context, request): - return request.is_xhr - xhr_predicate.__text__ = "xhr = True" - weights.append(1 << 1) - predicates.append(xhr_predicate) - h.update(bytes_('xhr:%r' % bool(xhr))) - - if request_method is not None: - if not is_nonstr_iter(request_method): - request_method = (request_method,) - request_method = sorted(request_method) - def request_method_predicate(context, request): - return request.method in request_method - text = "request method = %r" % request_method - request_method_predicate.__text__ = text - weights.append(1 << 2) - predicates.append(request_method_predicate) - for m in request_method: - h.update(bytes_('request_method:%r' % m)) - - if path_info is not None: - try: - path_info_val = re.compile(path_info) - except re.error as why: - raise ConfigurationError(why.args[0]) - def path_info_predicate(context, request): - return path_info_val.match(request.upath_info) is not None - text = "path_info = %s" - path_info_predicate.__text__ = text % path_info - weights.append(1 << 3) - predicates.append(path_info_predicate) - h.update(bytes_('path_info:%r' % path_info)) - - if request_param is not None: - request_param_val = None - if '=' in request_param: - request_param, request_param_val = request_param.split('=', 1) - if request_param_val is None: - text = "request_param %s" % request_param - else: - text = "request_param %s = %s" % (request_param, request_param_val) - def request_param_predicate(context, request): - if request_param_val is None: - return request_param in request.params - return request.params.get(request_param) == request_param_val - request_param_predicate.__text__ = text - weights.append(1 << 4) - predicates.append(request_param_predicate) - h.update( - bytes_('request_param:%r=%r' % (request_param, request_param_val))) - - if header is not None: - header_name = header - header_val = None - if ':' in header: - header_name, header_val = header.split(':', 1) - try: - header_val = re.compile(header_val) - except re.error as why: - raise ConfigurationError(why.args[0]) - if header_val is None: - text = "header %s" % header_name - else: - text = "header %s = %s" % (header_name, header_val) - def header_predicate(context, request): - if header_val is None: - return header_name in request.headers - val = request.headers.get(header_name) - if val is None: - return False - return header_val.match(val) is not None - header_predicate.__text__ = text - weights.append(1 << 5) - predicates.append(header_predicate) - h.update(bytes_('header:%r=%r' % (header_name, header_val))) - - if accept is not None: - def accept_predicate(context, request): - return accept in request.accept - accept_predicate.__text__ = "accept = %s" % accept - weights.append(1 << 6) - predicates.append(accept_predicate) - h.update(bytes_('accept:%r' % accept)) - - if containment is not None: - def containment_predicate(context, request): - ctx = getattr(request, 'context', context) - return find_interface(ctx, containment) is not None - containment_predicate.__text__ = "containment = %s" % containment - weights.append(1 << 7) - predicates.append(containment_predicate) - h.update(bytes_('containment:%r' % hash(containment))) - - if request_type is not None: - def request_type_predicate(context, request): - return request_type.providedBy(request) - text = "request_type = %s" - request_type_predicate.__text__ = text % request_type - weights.append(1 << 8) - predicates.append(request_type_predicate) - h.update(bytes_('request_type:%r' % hash(request_type))) - - if match_param is not None: - if not is_nonstr_iter(match_param): - match_param = (match_param,) - match_param = sorted(match_param) - text = "match_param %s" % repr(match_param) - reqs = [p.split('=', 1) for p in match_param] - def match_param_predicate(context, request): - for k, v in reqs: - if request.matchdict.get(k) != v: - return False - return True - match_param_predicate.__text__ = text - weights.append(1 << 9) - predicates.append(match_param_predicate) - for p in match_param: - h.update(bytes_('match_param:%r' % p)) - - if custom: - for num, predicate in enumerate(custom): - if getattr(predicate, '__text__', None) is None: - text = '' - try: - predicate.__text__ = text - except AttributeError: - # if this happens the predicate is probably a classmethod - if hasattr(predicate, '__func__'): - predicate.__func__.__text__ = text - else: # pragma: no cover ; 2.5 doesn't have __func__ - predicate.im_func.__text__ = text - predicates.append(predicate) - # 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. - h.update(bytes_('custom%s:%r' % (num, hash(predicate)))) - weights.append(1 << 10) - - if traverse is not None: - # ``traverse`` 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. - from pyramid.urldispatch import _compile_route - _, tgenerate = _compile_route(traverse) - def traverse_predicate(context, request): - if 'traverse' in context: - return True - m = context['match'] - tvalue = tgenerate(m) # tvalue will be urlquoted string - m['traverse'] = traversal_path(tvalue) # will be seq of unicode - return True - traverse_predicate.__text__ = 'traverse matchdict pseudo-predicate' - # This isn't actually a predicate, it's just a infodict - # modifier that injects ``traverse`` into the matchdict. As a - # result, the ``traverse_predicate`` function above always - # returns True, and we don't need to update the hash or attach - # a weight to it - predicates.append(traverse_predicate) - - score = 0 - for bit in weights: - score = score | bit - order = (MAX_ORDER - score) / (len(predicates) + 1) - phash = h.hexdigest() - return order, predicates, phash - def as_sorted_tuple(val): if not is_nonstr_iter(val): val = (val,) @@ -334,16 +98,22 @@ class TopologicalSorter(object): self.last = last def remove(self, name): - if name in self.names: - self.names.remove(name) - del self.name2val[name] - for u in self.name2after.get(name, []): + self.names.remove(name) + del self.name2val[name] + after = self.name2after.pop(name, []) + if after: + self.req_after.remove(name) + for u in after: self.order.remove((u, name)) - for u in self.name2before.get(name, []): + before = self.name2before.pop(name, []) + if before: + self.req_before.remove(name) + for u in before: self.order.remove((name, u)) def add(self, name, val, after=None, before=None): - self.remove(name) + if name in self.names: + self.remove(name) self.names.append(name) self.name2val[name] = val if after is None and before is None: @@ -448,41 +218,89 @@ class CyclicDependencyError(Exception): return msg class PredicateList(object): + def __init__(self): self.sorter = TopologicalSorter() + self.last_added = None def add(self, name, factory, weighs_more_than=None, weighs_less_than=None): + # Predicates should be added to a predicate list in (presumed) + # computation expense order. + ## if weighs_more_than is None and weighs_less_than is None: + ## weighs_more_than = self.last_added or FIRST + ## weighs_less_than = LAST + self.last_added = name self.sorter.add(name, factory, after=weighs_more_than, before=weighs_less_than) - def make(self, **kw): + def make(self, config, **kw): + # Given a configurator and a list of keywords, a predicate list is + # computed. Elsewhere in the code, we evaluate predicates using a + # generator expression. All predicates associated with a view or + # route must evaluate true for the view or route to "match" during a + # request. The fastest predicate should be evaluated first, then the + # next fastest, and so on, as if one returns false, the remainder of + # the predicates won't need to be evaluated. + # + # While we compute predicates, we also compute a predicate hash (aka + # phash) that can be used by a caller to identify identical predicate + # lists. ordered = self.sorter.sorted() phash = md5() weights = [] - predicates = [] - for order, (name, predicate_factory) in enumerate(ordered): + preds = [] + for n, (name, predicate_factory) in enumerate(ordered): vals = kw.pop(name, None) - if vals is None: + if vals is None: # XXX should this be a sentinel other than None? continue - if not isinstance(vals, SequenceOfPredicateValues): + if not isinstance(vals, predvalseq): vals = (vals,) for val in vals: - predicate = predicate_factory(val) - hashes = predicate.phash() + pred = predicate_factory(val, config) + hashes = pred.phash() if not is_nonstr_iter(hashes): hashes = [hashes] for h in hashes: phash.update(bytes_(h)) - predicate = predicate_factory(val) - weights.append(1 << order) - predicates.append(predicate) + weights.append(1 << n+1) + preds.append(pred) if kw: raise ConfigurationError('Unknown predicate values: %r' % (kw,)) + # A "order" is computed for the predicate list. An order is + # a scoring. + # + # Each predicate is associated with a weight value. The weight of a + # predicate symbolizes the relative potential "importance" of the + # predicate to all other predicates. A larger weight indicates + # greater importance. + # + # All weights for a given predicate list are bitwise ORed together + # to create a "score"; this score is then subtracted from + # MAX_ORDER and divided by an integer representing the number of + # predicates+1 to determine the order. + # + # For views, the order represents the ordering in which a "multiview" + # ( a collection of views that share the same context/request/name + # triad but differ in other ways via predicates) will attempt to call + # its set of views. Views with lower orders will be tried first. + # The intent is to a) ensure that views with more predicates are + # always evaluated before views with fewer predicates and b) to + # ensure a stable call ordering of views that share the same number + # of predicates. Views which do not have any predicates get an order + # of MAX_ORDER, meaning that they will be tried very last. score = 0 for bit in weights: score = score | bit - order = (MAX_ORDER - score) / (len(predicates) + 1) - return order, predicates, phash.hexdigest() + order = (MAX_ORDER - score) / (len(preds) + 1) + return order, preds, phash.hexdigest() -class SequenceOfPredicateValues(tuple): +class predvalseq(tuple): pass + +class Deferred(object): + def __init__(self, func): + self.func = func + + def resolve(self): + return self.func() + diff --git a/pyramid/config/views.py b/pyramid/config/views.py index f2fe83673..3f0c5c7c8 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -69,12 +69,13 @@ from pyramid.util import object_description from pyramid.config import predicates from pyramid.config.util import ( + Deferred, DEFAULT_PHASH, MAX_ORDER, action_method, as_sorted_tuple, PredicateList, - SequenceOfPredicateValues, + predvalseq, ) urljoin = urlparse.urljoin @@ -635,13 +636,31 @@ def viewdefaults(wrapped): class ViewsConfiguratorMixin(object): @viewdefaults @action_method - def add_view(self, view=None, name="", for_=None, permission=None, - request_type=None, route_name=None, request_method=None, - request_param=None, containment=None, attr=None, - renderer=None, wrapper=None, xhr=None, accept=None, - header=None, path_info=None, custom_predicates=(), - context=None, decorator=None, mapper=None, http_cache=None, - match_param=None, **other_predicates): + def add_view( + self, + view=None, + name="", + for_=None, + permission=None, + request_type=None, + route_name=None, + request_method=None, + request_param=None, + containment=None, + attr=None, + renderer=None, + wrapper=None, + xhr=None, + accept=None, + header=None, + path_info=None, + custom_predicates=(), + context=None, + decorator=None, + mapper=None, + http_cache=None, + match_param=None, + **other_predicates): """ Add a :term:`view configuration` to the current configuration state. Arguments to ``add_view`` are broken down below into *predicate* arguments and *non-predicate* @@ -664,24 +683,27 @@ class ViewsConfiguratorMixin(object): permission - The name of a :term:`permission` that the user must possess - in order to invoke the :term:`view callable`. See - :ref:`view_security_section` for more information about view - security and permissions. If ``permission`` is omitted, a - *default* permission may be used for this view registration - if one was named as the + A :term:`permission` that the user must possess in order to invoke + the :term:`view callable`. See :ref:`view_security_section` for + more information about view security and permissions. This is + often a string like ``view`` or ``edit``. + + If ``permission`` is omitted, a *default* permission may be used + for this view registration if one was named as the :class:`pyramid.config.Configurator` constructor's ``default_permission`` argument, or if - :meth:`pyramid.config.Configurator.set_default_permission` - was used prior to this view registration. Pass the string - :data:`pyramid.security.NO_PERMISSION_REQUIRED` as the - permission argument to explicitly indicate that the view should - always be executable by entirely anonymous users, regardless of - the default permission, bypassing any :term:`authorization - policy` that may be in effect. + :meth:`pyramid.config.Configurator.set_default_permission` was used + prior to this view registration. Pass the value + :data:`pyramid.security.NO_PERMISSION_REQUIRED` as the permission + argument to explicitly indicate that the view should always be + executable by entirely anonymous users, regardless of the default + permission, bypassing any :term:`authorization policy` that may be + in effect. attr + This knob is most useful when the view definition is a class. + The view machinery defaults to using the ``__call__`` method of the :term:`view callable` (or the function itself, if the view callable is a function) to obtain a response. The @@ -690,8 +712,7 @@ class ViewsConfiguratorMixin(object): class, and the class has a method named ``index`` and you wanted to use this method instead of the class' ``__call__`` method to return the response, you'd say ``attr="index"`` in the - view configuration for the view. This is - most useful when the view definition is a class. + view configuration for the view. renderer @@ -975,9 +996,15 @@ class ViewsConfiguratorMixin(object): Each custom predicate callable should accept two arguments: ``context`` and ``request`` and should return either ``True`` or ``False`` after doing arbitrary evaluation of - the context and/or the request. If all callables return - ``True``, the associated view callable will be considered - viable for a given request. + the context and/or the request. + + other_predicates + + Pass a key/value pair here to use a third-party predicate registered + via :meth:`pyramid.config.Configurator.add_view_predicate`. More + than one key/value pair can be used at the same time. See + :ref:`registering_thirdparty_predicates` for more information + about third-party predicates. """ view = self.maybe_dotted(view) @@ -1034,17 +1061,26 @@ class ViewsConfiguratorMixin(object): containment=containment, request_type=request_type, match_param=match_param, - custom=SequenceOfPredicateValues(custom_predicates), + custom=predvalseq(custom_predicates), ) ) - - discriminator = ('view', context, name, route_name, attr, - str(sorted(pvals.items()))) + + def discrim_func(): + # We need to defer the discriminator until we know what the phash + # is. It can't be computed any sooner because thirdparty + # predicates may not yet exist when add_view is called. + order, preds, phash = predlist.make(self, **pvals) + view_intr.update({'phash':phash, 'order':order, 'predicates':preds}) + return ('view', context, name, route_name, phash) + + discriminator = Deferred(discrim_func) + if inspect.isclass(view) and attr: view_desc = 'method %r of %s' % ( attr, self.object_description(view)) else: view_desc = self.object_description(view) + view_intr = self.introspectable('views', discriminator, view_desc, @@ -1072,8 +1108,10 @@ class ViewsConfiguratorMixin(object): predlist = self.view_predlist def register(permission=permission, renderer=renderer): - order, preds, phash = predlist.make(**pvals) - view_intr.update({'phash':phash}) + # the discrim_func above is guaranteed to have been called already + order = view_intr['order'] + preds = view_intr['predicates'] + phash = view_intr['phash'] request_iface = IRequest if route_name is not None: request_iface = self.registry.queryUtility(IRouteRequest, @@ -1098,21 +1136,28 @@ class ViewsConfiguratorMixin(object): # (reg'd in phase 1) permission = self.registry.queryUtility(IDefaultPermission) + # added by discrim_func above during conflict resolving + preds = view_intr['predicates'] + order = view_intr['order'] + phash = view_intr['phash'] + # __no_permission_required__ handled by _secure_view - deriver = ViewDeriver(registry=self.registry, - permission=permission, - predicates=preds, - attr=attr, - renderer=renderer, - wrapper_viewname=wrapper, - viewname=name, - accept=accept, - order=order, - phash=phash, - package=self.package, - mapper=mapper, - decorator=decorator, - http_cache=http_cache) + deriver = ViewDeriver( + registry=self.registry, + permission=permission, + predicates=preds, + attr=attr, + renderer=renderer, + wrapper_viewname=wrapper, + viewname=name, + accept=accept, + order=order, + phash=phash, + package=self.package, + mapper=mapper, + decorator=decorator, + http_cache=http_cache, + ) derived_view = deriver(view) derived_view.__discriminator__ = lambda *arg: discriminator # __discriminator__ is used by superdynamic systems @@ -1217,19 +1262,25 @@ class ViewsConfiguratorMixin(object): IMultiView, name=name) if mapper: - mapper_intr = self.introspectable('view mappers', - discriminator, - 'view mapper for %s' % view_desc, - 'view mapper') + mapper_intr = self.introspectable( + 'view mappers', + discriminator, + 'view mapper for %s' % view_desc, + 'view mapper' + ) mapper_intr['mapper'] = mapper mapper_intr.relate('views', discriminator) introspectables.append(mapper_intr) if route_name: view_intr.relate('routes', route_name) # see add_route if renderer is not None and renderer.name and '.' in renderer.name: - # it's a template - tmpl_intr = self.introspectable('templates', discriminator, - renderer.name, 'template') + # the renderer is a template + tmpl_intr = self.introspectable( + 'templates', + discriminator, + renderer.name, + 'template' + ) tmpl_intr.relate('views', discriminator) tmpl_intr['name'] = renderer.name tmpl_intr['type'] = renderer.type @@ -1237,8 +1288,13 @@ class ViewsConfiguratorMixin(object): tmpl_intr.relate('renderer factories', renderer.type) introspectables.append(tmpl_intr) if permission is not None: - perm_intr = self.introspectable('permissions', permission, - permission, 'permission') + # if a permission exists, register a permission introspectable + perm_intr = self.introspectable( + 'permissions', + permission, + permission, + 'permission' + ) perm_intr['value'] = permission perm_intr.relate('views', discriminator) introspectables.append(perm_intr) @@ -1255,13 +1311,14 @@ class ViewsConfiguratorMixin(object): @action_method def add_view_predicate(self, name, factory, weighs_more_than=None, weighs_less_than=None): - """ Adds a view predicate factory. The view predicate can later be - named as a keyword argument to - :meth:`pyramid.config.Configurator.add_view`. + """ Adds a view predicate factory. The associated view predicate can + later be named as a keyword argument to + :meth:`pyramid.config.Configurator.add_view` in the + ``other_predicates`` anonyous keyword argument dictionary. ``name`` should be the name of the predicate. It must be a valid Python identifier (it will be used as a keyword argument to - ``add_view``). + ``add_view`` by others). ``factory`` should be a :term:`predicate factory`. """ @@ -1283,26 +1340,19 @@ class ViewsConfiguratorMixin(object): order=PHASE1_CONFIG) # must be registered before views added def add_default_view_predicates(self): - self.add_view_predicate( - 'xhr', predicates.XHRPredicate) - self.add_view_predicate( - 'request_method', predicates.RequestMethodPredicate) - self.add_view_predicate( - 'path_info', predicates.PathInfoPredicate) - self.add_view_predicate( - 'request_param', predicates.RequestParamPredicate) - self.add_view_predicate( - 'header', predicates.HeaderPredicate) - self.add_view_predicate( - 'accept', predicates.AcceptPredicate) - self.add_view_predicate( - 'containment', predicates.ContainmentPredicate) - self.add_view_predicate( - 'request_type', predicates.RequestTypePredicate) - self.add_view_predicate( - 'match_param', predicates.MatchParamPredicate) - self.add_view_predicate( - 'custom', predicates.CustomPredicate) + for (name, factory) in ( + ('xhr', predicates.XHRPredicate), + ('request_method', predicates.RequestMethodPredicate), + ('path_info', predicates.PathInfoPredicate), + ('request_param', predicates.RequestParamPredicate), + ('header', predicates.HeaderPredicate), + ('accept', predicates.AcceptPredicate), + ('containment', predicates.ContainmentPredicate), + ('request_type', predicates.RequestTypePredicate), + ('match_param', predicates.MatchParamPredicate), + ('custom', predicates.CustomPredicate), + ): + self.add_view_predicate(name, factory) def derive_view(self, view, attr=None, renderer=None): """ diff --git a/pyramid/testing.py b/pyramid/testing.py index 2628dc817..89eec84b0 100644 --- a/pyramid/testing.py +++ b/pyramid/testing.py @@ -372,6 +372,7 @@ def registerRoute(pattern, name, factory=None): """ reg = get_current_registry() config = Configurator(registry=reg) + config.setup_registry() result = config.add_route(name, pattern, factory=factory) config.commit() return result @@ -825,6 +826,7 @@ def setUp(registry=None, request=None, hook_zca=True, autocommit=True, # any existing renderer factory lookup system. config.add_renderer(name, renderer) config.add_default_view_predicates() + config.add_default_route_predicates() config.commit() global have_zca try: diff --git a/pyramid/tests/test_config/test_init.py b/pyramid/tests/test_config/test_init.py index b23168aaa..3d952caf7 100644 --- a/pyramid/tests/test_config/test_init.py +++ b/pyramid/tests/test_config/test_init.py @@ -1396,13 +1396,9 @@ class TestConfiguratorDeprecatedFeatures(unittest.TestCase): try: config.commit() except ConfigurationConflictError as why: - c1, c2, c3, c4, c5, c6 = _conflictFunctions(why) + c1, c2 = _conflictFunctions(why) self.assertEqual(c1, 'test_conflict_route_with_view') self.assertEqual(c2, 'test_conflict_route_with_view') - self.assertEqual(c3, 'test_conflict_route_with_view') - self.assertEqual(c4, 'test_conflict_route_with_view') - self.assertEqual(c5, 'test_conflict_route_with_view') - self.assertEqual(c6, 'test_conflict_route_with_view') else: # pragma: no cover raise AssertionError @@ -1693,6 +1689,7 @@ class Test_resolveConflicts(unittest.TestCase): (3, f, (3,), {}, ('y',)), (None, f, (5,), {}, ('y',)), ]) + result = list(result) self.assertEqual( result, [{'info': None, @@ -1754,6 +1751,7 @@ class Test_resolveConflicts(unittest.TestCase): expand_action(3, f, (3,), {}, ('y',)), expand_action(None, f, (5,), {}, ('y',)), ]) + result = list(result) self.assertEqual( result, [{'info': None, @@ -1805,32 +1803,31 @@ class Test_resolveConflicts(unittest.TestCase): def test_it_conflict(self): from pyramid.tests.test_config import dummyfactory as f - self.assertRaises( - ConfigurationConflictError, - self._callFUT, [ - (None, f), - (1, f, (2,), {}, ('x',), 'eek'), - (1, f, (3,), {}, ('y',), 'ack'), - (4, f, (4,), {}, ('y',)), - (3, f, (3,), {}, ('y',)), - (None, f, (5,), {}, ('y',)), - ] - ) + result = self._callFUT([ + (None, f), + (1, f, (2,), {}, ('x',), 'eek'), # will conflict + (1, f, (3,), {}, ('y',), 'ack'), # will conflict + (4, f, (4,), {}, ('y',)), + (3, f, (3,), {}, ('y',)), + (None, f, (5,), {}, ('y',)), + ]) + self.assertRaises(ConfigurationConflictError, list, result) def test_it_with_actions_grouped_by_order(self): from pyramid.tests.test_config import dummyfactory as f from pyramid.config import expand_action result = self._callFUT([ - expand_action(None, f), - expand_action(1, f, (1,), {}, (), 'third', 10), + expand_action(None, f), # X + expand_action(1, f, (1,), {}, (), 'third', 10), # X expand_action(1, f, (2,), {}, ('x',), 'fourth', 10), expand_action(1, f, (3,), {}, ('y',), 'fifth', 10), - expand_action(2, f, (1,), {}, (), 'sixth', 10), - expand_action(3, f, (1,), {}, (), 'seventh', 10), - expand_action(5, f, (4,), {}, ('y',), 'eighth', 99999), - expand_action(4, f, (3,), {}, (), 'first', 5), + expand_action(2, f, (1,), {}, (), 'sixth', 10), # X + expand_action(3, f, (1,), {}, (), 'seventh', 10), # X + expand_action(5, f, (4,), {}, ('y',), 'eighth', 99999), # X + expand_action(4, f, (3,), {}, (), 'first', 5), # X expand_action(4, f, (5,), {}, ('y',), 'second', 5), ]) + result = list(result) self.assertEqual(len(result), 6) # resolved actions should be grouped by (order, i) self.assertEqual( diff --git a/pyramid/tests/test_config/test_predicates.py b/pyramid/tests/test_config/test_predicates.py index 8a9da7a41..79dabd5d4 100644 --- a/pyramid/tests/test_config/test_predicates.py +++ b/pyramid/tests/test_config/test_predicates.py @@ -5,7 +5,7 @@ from pyramid.compat import text_ class TestXHRPredicate(unittest.TestCase): def _makeOne(self, val): from pyramid.config.predicates import XHRPredicate - return XHRPredicate(val) + return XHRPredicate(val, None) def test___call___true(self): inst = self._makeOne(True) @@ -32,7 +32,7 @@ class TestXHRPredicate(unittest.TestCase): class TestRequestMethodPredicate(unittest.TestCase): def _makeOne(self, val): from pyramid.config.predicates import RequestMethodPredicate - return RequestMethodPredicate(val) + return RequestMethodPredicate(val, None) def test___call___true_single(self): inst = self._makeOne('GET') @@ -66,7 +66,7 @@ class TestRequestMethodPredicate(unittest.TestCase): class TestPathInfoPredicate(unittest.TestCase): def _makeOne(self, val): from pyramid.config.predicates import PathInfoPredicate - return PathInfoPredicate(val) + return PathInfoPredicate(val, None) def test_ctor_compilefail(self): from pyramid.exceptions import ConfigurationError @@ -97,7 +97,7 @@ class TestPathInfoPredicate(unittest.TestCase): class TestRequestParamPredicate(unittest.TestCase): def _makeOne(self, val): from pyramid.config.predicates import RequestParamPredicate - return RequestParamPredicate(val) + return RequestParamPredicate(val, None) def test___call___true_exists(self): inst = self._makeOne('abc') @@ -139,7 +139,7 @@ class TestRequestParamPredicate(unittest.TestCase): class TestMatchParamPredicate(unittest.TestCase): def _makeOne(self, val): from pyramid.config.predicates import MatchParamPredicate - return MatchParamPredicate(val) + return MatchParamPredicate(val, None) def test___call___true_single(self): inst = self._makeOne('abc=1') @@ -174,7 +174,7 @@ class TestMatchParamPredicate(unittest.TestCase): class TestCustomPredicate(unittest.TestCase): def _makeOne(self, val): from pyramid.config.predicates import CustomPredicate - return CustomPredicate(val) + return CustomPredicate(val, None) def test___call___true(self): def func(context, request): @@ -203,17 +203,17 @@ class TestCustomPredicate(unittest.TestCase): def test_text_func_repr(self): pred = predicate() inst = self._makeOne(pred) - self.assertTrue(inst.text(), 'predicate') + self.assertEqual(inst.text(), u'custom predicate: object predicate') def test_phash(self): pred = predicate() inst = self._makeOne(pred) - self.assertTrue(inst.phash(), 'custom:1') + self.assertEqual(inst.phash(), 'custom:1') class TestTraversePredicate(unittest.TestCase): def _makeOne(self, val): from pyramid.config.predicates import TraversePredicate - return TraversePredicate(val) + return TraversePredicate(val, None) def test___call__traverse_has_remainder_already(self): inst = self._makeOne('/1/:a/:b') diff --git a/pyramid/tests/test_config/test_routes.py b/pyramid/tests/test_config/test_routes.py index bb47d2d7e..6fb5189f6 100644 --- a/pyramid/tests/test_config/test_routes.py +++ b/pyramid/tests/test_config/test_routes.py @@ -158,7 +158,7 @@ class RoutesConfiguratorMixinTests(unittest.TestCase): def pred2(context, request): pass config.add_route('name', 'path', custom_predicates=(pred1, pred2)) route = self._assertRoute(config, 'name', 'path', 2) - self.assertEqual(route.predicates, [pred1, pred2]) + self.assertEqual(len(route.predicates), 2) def test_add_route_with_header(self): config = self._makeOne(autocommit=True) diff --git a/pyramid/tests/test_config/test_util.py b/pyramid/tests/test_config/test_util.py index 06e63ca40..310d04535 100644 --- a/pyramid/tests/test_config/test_util.py +++ b/pyramid/tests/test_config/test_util.py @@ -1,10 +1,32 @@ import unittest from pyramid.compat import text_ -class Test__make_predicates(unittest.TestCase): +class TestPredicateList(unittest.TestCase): + + def _makeOne(self): + from pyramid.config.util import PredicateList + from pyramid.config import predicates + inst = PredicateList() + for name, factory in ( + ('xhr', predicates.XHRPredicate), + ('request_method', predicates.RequestMethodPredicate), + ('path_info', predicates.PathInfoPredicate), + ('request_param', predicates.RequestParamPredicate), + ('header', predicates.HeaderPredicate), + ('accept', predicates.AcceptPredicate), + ('containment', predicates.ContainmentPredicate), + ('request_type', predicates.RequestTypePredicate), + ('match_param', predicates.MatchParamPredicate), + ('custom', predicates.CustomPredicate), + ('traverse', predicates.TraversePredicate), + ): + inst.add(name, factory) + return inst + def _callFUT(self, **kw): - from pyramid.config.util import make_predicates - return make_predicates(**kw) + inst = self._makeOne() + config = DummyConfigurator() + return inst.make(config, **kw) def test_ordering_xhr_and_request_method_trump_only_containment(self): order1, _, _ = self._callFUT(xhr=True, request_method='GET') @@ -12,6 +34,7 @@ class Test__make_predicates(unittest.TestCase): self.assertTrue(order1 < order2) def test_ordering_number_of_predicates(self): + from pyramid.config.util import predvalseq order1, _, _ = self._callFUT( xhr='xhr', request_method='request_method', @@ -22,7 +45,7 @@ class Test__make_predicates(unittest.TestCase): accept='accept', containment='containment', request_type='request_type', - custom=(DummyCustomPredicate(),), + custom=predvalseq([DummyCustomPredicate()]), ) order2, _, _ = self._callFUT( xhr='xhr', @@ -34,7 +57,7 @@ class Test__make_predicates(unittest.TestCase): accept='accept', containment='containment', request_type='request_type', - custom=(DummyCustomPredicate(),), + custom=predvalseq([DummyCustomPredicate()]), ) order3, _, _ = self._callFUT( xhr='xhr', @@ -114,6 +137,7 @@ class Test__make_predicates(unittest.TestCase): self.assertTrue(order12 > order10) def test_ordering_importance_of_predicates(self): + from pyramid.config.util import predvalseq order1, _, _ = self._callFUT( xhr='xhr', ) @@ -142,7 +166,7 @@ class Test__make_predicates(unittest.TestCase): match_param='foo=bar', ) order10, _, _ = self._callFUT( - custom=(DummyCustomPredicate(),), + custom=predvalseq([DummyCustomPredicate()]), ) self.assertTrue(order1 > order2) self.assertTrue(order2 > order3) @@ -155,12 +179,13 @@ class Test__make_predicates(unittest.TestCase): self.assertTrue(order9 > order10) def test_ordering_importance_and_number(self): + from pyramid.config.util import predvalseq order1, _, _ = self._callFUT( xhr='xhr', request_method='request_method', ) order2, _, _ = self._callFUT( - custom=(DummyCustomPredicate(),), + custom=predvalseq([DummyCustomPredicate()]), ) self.assertTrue(order1 < order2) @@ -170,7 +195,7 @@ class Test__make_predicates(unittest.TestCase): ) order2, _, _ = self._callFUT( request_method='request_method', - custom=(DummyCustomPredicate(),), + custom=predvalseq([DummyCustomPredicate()]), ) self.assertTrue(order1 > order2) @@ -181,7 +206,7 @@ class Test__make_predicates(unittest.TestCase): ) order2, _, _ = self._callFUT( request_method='request_method', - custom=(DummyCustomPredicate(),), + custom=predvalseq([DummyCustomPredicate()]), ) self.assertTrue(order1 < order2) @@ -193,18 +218,19 @@ class Test__make_predicates(unittest.TestCase): order2, _, _ = self._callFUT( xhr='xhr', request_method='request_method', - custom=(DummyCustomPredicate(),), + custom=predvalseq([DummyCustomPredicate()]), ) self.assertTrue(order1 > order2) def test_different_custom_predicates_with_same_hash(self): + from pyramid.config.util import predvalseq class PredicateWithHash(object): def __hash__(self): return 1 a = PredicateWithHash() b = PredicateWithHash() - _, _, a_phash = self._callFUT(custom=(a,)) - _, _, b_phash = self._callFUT(custom=(b,)) + _, _, a_phash = self._callFUT(custom=predvalseq([a])) + _, _, b_phash = self._callFUT(custom=predvalseq([b])) self.assertEqual(a_phash, b_phash) def test_traverse_has_remainder_already(self): @@ -244,12 +270,14 @@ class Test__make_predicates(unittest.TestCase): ) def test_custom_predicates_can_affect_traversal(self): + from pyramid.config.util import predvalseq def custom(info, request): m = info['match'] m['dummy'] = 'foo' return True - _, predicates, _ = self._callFUT(custom=(custom,), - traverse='/1/:dummy/:a') + _, predicates, _ = self._callFUT( + custom=predvalseq([custom]), + traverse='/1/:dummy/:a') self.assertEqual(len(predicates), 2) info = {'match':{'a':'a'}} request = DummyRequest() @@ -259,6 +287,7 @@ class Test__make_predicates(unittest.TestCase): 'traverse':('1', 'foo', 'a')}}) def test_predicate_text_is_correct(self): + from pyramid.config.util import predvalseq _, predicates, _ = self._callFUT( xhr='xhr', request_method='request_method', @@ -268,23 +297,27 @@ class Test__make_predicates(unittest.TestCase): accept='accept', containment='containment', request_type='request_type', - custom=(DummyCustomPredicate(), + custom=predvalseq( + [ + DummyCustomPredicate(), DummyCustomPredicate.classmethod_predicate, - DummyCustomPredicate.classmethod_predicate_no_text), + DummyCustomPredicate.classmethod_predicate_no_text, + ] + ), match_param='foo=bar') - self.assertEqual(predicates[0].__text__, 'xhr = True') - self.assertEqual(predicates[1].__text__, - "request method = ['request_method']") - self.assertEqual(predicates[2].__text__, 'path_info = path_info') - self.assertEqual(predicates[3].__text__, 'request_param param') - self.assertEqual(predicates[4].__text__, 'header header') - self.assertEqual(predicates[5].__text__, 'accept = accept') - self.assertEqual(predicates[6].__text__, 'containment = containment') - self.assertEqual(predicates[7].__text__, 'request_type = request_type') - self.assertEqual(predicates[8].__text__, "match_param ['foo=bar']") - self.assertEqual(predicates[9].__text__, 'custom predicate') - self.assertEqual(predicates[10].__text__, 'classmethod predicate') - self.assertEqual(predicates[11].__text__, '') + self.assertEqual(predicates[0].text(), 'xhr = True') + self.assertEqual(predicates[1].text(), + "request_method = request_method") + self.assertEqual(predicates[2].text(), 'path_info = path_info') + self.assertEqual(predicates[3].text(), 'request_param param') + self.assertEqual(predicates[4].text(), 'header header') + self.assertEqual(predicates[5].text(), 'accept = accept') + self.assertEqual(predicates[6].text(), 'containment = containment') + self.assertEqual(predicates[7].text(), 'request_type = request_type') + self.assertEqual(predicates[8].text(), "match_param foo=bar") + self.assertEqual(predicates[9].text(), 'custom predicate') + self.assertEqual(predicates[10].text(), 'classmethod predicate') + self.assertTrue(predicates[11].text().startswith('custom predicate')) def test_match_param_from_string(self): _, predicates, _ = self._callFUT(match_param='foo=bar') @@ -641,3 +674,7 @@ class DummyRequest: self.params = {} self.cookies = {} +class DummyConfigurator(object): + def maybe_dotted(self, thing): + return thing + diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index 9ff83e956..f2daf0c34 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -118,7 +118,8 @@ class TestViewsConfigurationMixin(unittest.TestCase): self.assertEqual(wrapper.__module__, view.__module__) self.assertEqual(wrapper.__name__, view.__name__) self.assertEqual(wrapper.__doc__, view.__doc__) - self.assertEqual(wrapper.__discriminator__(None, None)[0], 'view') + self.assertEqual(wrapper.__discriminator__(None, None).resolve()[0], + 'view') def test_add_view_view_callable_dottedname(self): from pyramid.renderers import null_renderer @@ -970,8 +971,10 @@ class TestViewsConfigurationMixin(unittest.TestCase): wrapper = self._getViewCallable(config) self.assertTrue(IMultiView.providedBy(wrapper)) request = self._makeRequest(config) - self.assertTrue('IFoo' in wrapper.__discriminator__(foo, request)[5]) - self.assertTrue('IBar' in wrapper.__discriminator__(bar, request)[5]) + self.assertNotEqual( + wrapper.__discriminator__(foo, request), + wrapper.__discriminator__(bar, request), + ) def test_add_view_with_template_renderer(self): from pyramid.tests import test_config diff --git a/pyramid/tests/test_testing.py b/pyramid/tests/test_testing.py index 5b0073b81..a9e50442f 100644 --- a/pyramid/tests/test_testing.py +++ b/pyramid/tests/test_testing.py @@ -253,6 +253,7 @@ class Test_registerSubscriber(TestBase): class Test_registerRoute(TestBase): def test_registerRoute(self): + from pyramid.config import Configurator from pyramid.request import Request from pyramid.interfaces import IRoutesMapper from pyramid.testing import registerRoute @@ -261,6 +262,8 @@ class Test_registerRoute(TestBase): self.assertEqual(len(mapper.routelist), 1) request = Request.blank('/') request.registry = self.registry + config = Configurator(registry=self.registry) + config.setup_registry() self.assertEqual(request.route_url('home', pagename='abc'), 'http://localhost/abc') -- cgit v1.2.3