diff options
25 files changed, 1667 insertions, 624 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index 94553955c..ecffea5d9 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -26,6 +26,21 @@ Bug Fixes Features -------- +- Third-party custom view and route predicates can now be added for use by + view authors via ``pyramid.config.Configurator.add_view_predicate`` and + ``pyramid.config.Configurator.add_route_predicate``. So, for example, + doing this:: + + config.add_view_predicate('abc', my.package.ABCPredicate) + + Might allow a view author to do this in an application that configured that + predicate:: + + @view_config(abc=1) + + See "Adding A Third Party View or Route Predicate" in the Hooks chapter for + more information. + - Custom objects can be made easily JSON-serializable in Pyramid by defining a ``__json__`` method on the object's class. This method should return values natively serializable by ``json.dumps`` (such as ints, lists, diff --git a/docs/api/config.rst b/docs/api/config.rst index cd58e74d3..bc9e067b1 100644 --- a/docs/api/config.rst +++ b/docs/api/config.rst @@ -66,6 +66,8 @@ .. automethod:: add_response_adapter .. automethod:: add_traverser .. automethod:: add_tween + .. automethod:: add_route_predicate + .. automethod:: add_view_predicate .. automethod:: set_request_factory .. automethod:: set_root_factory .. automethod:: set_session_factory diff --git a/docs/glossary.rst b/docs/glossary.rst index 45a79326f..ba3203f89 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -994,3 +994,9 @@ Glossary Aka ``gunicorn``, a fast :term:`WSGI` server that runs on UNIX under Python 2.5+ (although at the time of this writing does not support Python 3). See http://gunicorn.org/ for detailed information. + + predicate factory + A callable which is used by a third party during the registration of a + route or view predicates to extend the view and route configuration + system. See :ref:`registering_thirdparty_predicates` for more + information. diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 332805152..bdd968362 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -1232,3 +1232,97 @@ Displaying Tween Ordering The ``ptweens`` command-line utility can be used to report the current implict and explicit tween chains used by an application. See :ref:`displaying_tweens`. + +.. _registering_thirdparty_predicates: + +Adding A Third Party View or Route Predicate +-------------------------------------------- + +View and route predicates used during view configuration allow you to narrow +the set of circumstances under which a view or route will match. For +example, the ``request_method`` view predicate can be used to ensure a view +callable is only invoked when the request's method is ``POST``: + +.. code-block:: python + + @view_config(request_method='POST') + def someview(request): + ... + +Likewise, a similar predicate can be used as a *route* predicate: + +.. code-block:: python + + config.add_route('name', '/foo', request_method='POST') + +Many other built-in predicates exists (``request_param``, and others). You +can add third-party predicates to the list of available predicates by using +one of :meth:`pyramid.config.Configurator.add_view_predicate` or +:meth:`pyramid.config.Configurator.add_route_predicate`. The former adds a +view predicate, the latter a route predicate. + +When using one of those APIs, you pass a *name* and a *factory* to add a +predicate during Pyramid's configuration stage. For example: + +.. code-block:: python + + config.add_view_predicate('content_type', ContentTypePredicate) + +The above example adds a new predicate named ``content_type`` to the list of +available predicates for views. This will allow the following view +configuration statement to work: + +.. code-block:: python + :linenos: + + @view_config(content_type='File') + def aview(request): ... + +The first argument to :meth:`pyramid.config.Configurator.add_view_predicate`, +the name, is a string representing the name that is expected to be passed to +``view_config`` (or its imperative analogue ``add_view``). + +The second argument is a predicate factory. A predicate factory is most +often a class with a constructor (``__init__``), a ``text`` method, a +``phash`` method and a ``__call__`` method. For example: + +.. code-block:: python + :linenos: + + class ContentTypePredicate(object): + def __init__(self, val, config): + self.val + + def text(self): + return 'content_type = %s' % (self.val,) + + phash = text + + def __call__(self, context, request): + return getattr(context, 'content_type', None) == self.val + +The constructor of a predicate factory takes two arguments: ``val`` and +``config``. The ``val`` argument will be the argument passed to +``view_config`` (or ``add_view``). In the example above, it will be the +string ``File``. The second arg, ``config`` will be the Configurator +instance at the time of configuration. + +The ``text`` method must return a string. It should be useful to describe +the behavior of the predicate in error messages. + +The ``phash`` method must return a string or a sequence of strings. It's +most often the same as ``text``, as long as ``text`` uniquely describes the +predicate's name and the value passed to the constructor. If ``text`` is +more general, or doesn't describe things that way, ``phash`` should return a +string with the name and the value serialized. The result of ``phash`` is +not seen in output anywhere, it just informs the uniqueness constraints for +view configuration. + +The ``__call__`` method of a predicate factory must accept a resource +(``context``) and a request, and must return ``True`` or ``False``. It is +the "meat" of the predicate. + +You can use the same predicate factory as both a view predicate and as a +route predicate, but you'll need to call ``add_view_predicate`` and +``add_route_predicate`` separately with the same factory. + diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index 52d7aca83..7e6649c14 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 @@ -353,6 +355,9 @@ class Configurator( for name, renderer in DEFAULT_RENDERERS: 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) self.add_view(exceptionresponse_view, context=IExceptionResponse) @@ -547,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: @@ -1058,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 @@ -1070,73 +1084,96 @@ 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(v): + n, v = v + if not isinstance(v, dict): + # old-style tuple action + v = expand_action(*v) + return (v['order'] or 0, n) + + sactions = sorted(enumerate(actions), key=orderandpos) + + def orderonly(v): + n, v = v + if not isinstance(v, dict): # old-style tuple action - action = expand_action(*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 new file mode 100644 index 000000000..311d47860 --- /dev/null +++ b/pyramid/config/predicates.py @@ -0,0 +1,224 @@ +import re + +from pyramid.compat import is_nonstr_iter + +from pyramid.exceptions import ConfigurationError + +from pyramid.traversal import ( + find_interface, + traversal_path, + ) + +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, 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): + self.val = as_sorted_tuple(val) + + 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): + name = val + v = None + if '=' in name: + name, v = name.split('=', 1) + name, v = name.strip(), v.strip() + if v is None: + self._text = 'request_param %s' % (name,) + else: + self._text = 'request_param %s = %s' % (name, v) + 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.params + return request.params.get(self.name) == self.val + + +class HeaderPredicate(object): + def __init__(self, val, config): + name = val + v = None + if ':' in name: + name, v = name.split(':', 1) + try: + v = re.compile(v) + 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, v) + 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): + if not is_nonstr_iter(val): + val = (val,) + val = sorted(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): + 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 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/tweens.py b/pyramid/config/tweens.py index 1a83f0de9..1bc6dc95c 100644 --- a/pyramid/config/tweens.py +++ b/pyramid/config/tweens.py @@ -16,7 +16,10 @@ from pyramid.tweens import ( EXCVIEW, ) -from pyramid.config.util import action_method +from pyramid.config.util import ( + action_method, + TopologicalSorter, + ) class TweensConfiguratorMixin(object): def add_tween(self, tween_factory, under=None, over=None): @@ -177,119 +180,24 @@ class TweensConfiguratorMixin(object): introspectables.append(intr) self.action(discriminator, register, introspectables=introspectables) -class CyclicDependencyError(Exception): - def __init__(self, cycles): - self.cycles = cycles - - def __str__(self): - L = [] - cycles = self.cycles - for cycle in cycles: - dependent = cycle - dependees = cycles[cycle] - L.append('%r sorts over %r' % (dependent, dependees)) - msg = 'Implicit tween ordering cycle:' + '; '.join(L) - return msg - @implementer(ITweens) class Tweens(object): def __init__(self): + self.sorter = TopologicalSorter( + default_before=None, + default_after=INGRESS, + first=INGRESS, + last=MAIN) self.explicit = [] - self.names = [] - self.req_over = set() - self.req_under = set() - self.factories = {} - self.order = [] def add_explicit(self, name, factory): self.explicit.append((name, factory)) def add_implicit(self, name, factory, under=None, over=None): - self.names.append(name) - self.factories[name] = factory - if under is None and over is None: - under = INGRESS - if under is not None: - if not is_nonstr_iter(under): - under = (under,) - self.order += [(u, name) for u in under] - self.req_under.add(name) - if over is not None: - if not is_nonstr_iter(over): - over = (over,) - self.order += [(name, o) for o in over] - self.req_over.add(name) + self.sorter.add(name, factory, after=under, before=over) def implicit(self): - order = [(INGRESS, MAIN)] - roots = [] - graph = {} - names = [INGRESS, MAIN] - names.extend(self.names) - - for a, b in self.order: - order.append((a, b)) - - def add_node(node): - if not node in graph: - roots.append(node) - graph[node] = [0] # 0 = number of arcs coming into this node - - def add_arc(fromnode, tonode): - graph[fromnode].append(tonode) - graph[tonode][0] += 1 - if tonode in roots: - roots.remove(tonode) - - for name in names: - add_node(name) - - has_over, has_under = set(), set() - for a, b in order: - if a in names and b in names: # deal with missing dependencies - add_arc(a, b) - has_over.add(a) - has_under.add(b) - - if not self.req_over.issubset(has_over): - raise ConfigurationError( - 'Detected tweens with no satisfied over dependencies: %s' - % (', '.join(sorted(self.req_over - has_over))) - ) - if not self.req_under.issubset(has_under): - raise ConfigurationError( - 'Detected tweens with no satisfied under dependencies: %s' - % (', '.join(sorted(self.req_under - has_under))) - ) - - sorted_names = [] - - while roots: - root = roots.pop(0) - sorted_names.append(root) - children = graph[root][1:] - for child in children: - arcs = graph[child][0] - arcs -= 1 - graph[child][0] = arcs - if arcs == 0: - roots.insert(0, child) - del graph[root] - - if graph: - # loop in input - cycledeps = {} - for k, v in graph.items(): - cycledeps[k] = v[1:] - raise CyclicDependencyError(cycledeps) - - result = [] - - for name in sorted_names: - if name in self.names: - result.append((name, self.factories[name])) - - return result + return self.sorter.sorted() def __call__(self, handler, registry): if self.explicit: diff --git a/pyramid/config/util.py b/pyramid/config/util.py index 4e4c93be3..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,239 +58,249 @@ 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 = '<unknown custom predicate>' - 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,) val = tuple(sorted(val)) return val +# under = after +# over = before + +class Singleton(object): + def __init__(self, repr): + self.repr = repr + + def __repr__(self): + return self.repr + +FIRST = Singleton('FIRST') +LAST = Singleton('LAST') + +class TopologicalSorter(object): + def __init__( + self, + default_before=LAST, + default_after=None, + first=FIRST, + last=LAST, + ): + self.names = [] + self.req_before = set() + self.req_after = set() + self.name2before = {} + self.name2after = {} + self.name2val = {} + self.order = [] + self.default_before = default_before + self.default_after = default_after + self.first = first + self.last = last + + def remove(self, 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)) + 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): + if name in self.names: + self.remove(name) + self.names.append(name) + self.name2val[name] = val + if after is None and before is None: + before = self.default_before + after = self.default_after + if after is not None: + if not is_nonstr_iter(after): + after = (after,) + self.name2after[name] = after + self.order += [(u, name) for u in after] + self.req_after.add(name) + if before is not None: + if not is_nonstr_iter(before): + before = (before,) + self.name2before[name] = before + self.order += [(name, o) for o in before] + self.req_before.add(name) + + def sorted(self): + order = [(self.first, self.last)] + roots = [] + graph = {} + names = [self.first, self.last] + names.extend(self.names) + + for a, b in self.order: + order.append((a, b)) + + def add_node(node): + if not node in graph: + roots.append(node) + graph[node] = [0] # 0 = number of arcs coming into this node + + def add_arc(fromnode, tonode): + graph[fromnode].append(tonode) + graph[tonode][0] += 1 + if tonode in roots: + roots.remove(tonode) + + for name in names: + add_node(name) + + has_before, has_after = set(), set() + for a, b in order: + if a in names and b in names: # deal with missing dependencies + add_arc(a, b) + has_before.add(a) + has_after.add(b) + + if not self.req_before.issubset(has_before): + raise ConfigurationError( + 'Unsatisfied before dependencies: %s' + % (', '.join(sorted(self.req_before - has_before))) + ) + if not self.req_after.issubset(has_after): + raise ConfigurationError( + 'Unsatisfied after dependencies: %s' + % (', '.join(sorted(self.req_after - has_after))) + ) + + sorted_names = [] + + while roots: + root = roots.pop(0) + sorted_names.append(root) + children = graph[root][1:] + for child in children: + arcs = graph[child][0] + arcs -= 1 + graph[child][0] = arcs + if arcs == 0: + roots.insert(0, child) + del graph[root] + + if graph: + # loop in input + cycledeps = {} + for k, v in graph.items(): + cycledeps[k] = v[1:] + raise CyclicDependencyError(cycledeps) + + result = [] + + for name in sorted_names: + if name in self.names: + result.append((name, self.name2val[name])) + + return result + +class CyclicDependencyError(Exception): + def __init__(self, cycles): + self.cycles = cycles + + def __str__(self): + L = [] + cycles = self.cycles + for cycle in cycles: + dependent = cycle + dependees = cycles[cycle] + L.append('%r sorts before %r' % (dependent, dependees)) + msg = 'Implicit ordering cycle:' + '; '.join(L) + 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, 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 = [] + preds = [] + for n, (name, predicate_factory) in enumerate(ordered): + vals = kw.pop(name, None) + if vals is None: # XXX should this be a sentinel other than None? + continue + if not isinstance(vals, predvalseq): + vals = (vals,) + for val in vals: + pred = predicate_factory(val, config) + hashes = pred.phash() + if not is_nonstr_iter(hashes): + hashes = [hashes] + for h in hashes: + phash.update(bytes_(h)) + 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(preds) + 1) + return order, preds, phash.hexdigest() + +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 4354b4691..3f0c5c7c8 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -20,6 +20,7 @@ from pyramid.interfaces import ( IException, IExceptionViewClassifier, IMultiView, + IPredicateList, IRendererFactory, IRequest, IResponse, @@ -65,12 +66,16 @@ from pyramid.view import ( 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, - make_predicates, + PredicateList, + predvalseq, ) urljoin = urlparse.urljoin @@ -272,22 +277,22 @@ class ViewDeriver(object): @wraps_view def predicated_view(self, view): - predicates = self.kw.get('predicates', ()) - if not predicates: + preds = self.kw.get('predicates', ()) + if not preds: return view def predicate_wrapper(context, request): - for predicate in predicates: + for predicate in preds: if not predicate(context, request): view_name = getattr(view, '__name__', view) raise PredicateMismatch( 'predicate mismatch for view %s (%s)' % ( - view_name, predicate.__text__)) + view_name, predicate.text())) return view(context, request) def checker(context, request): return all((predicate(context, request) for predicate in - predicates)) + preds)) predicate_wrapper.__predicated__ = checker - predicate_wrapper.__predicates__ = predicates + predicate_wrapper.__predicates__ = preds return predicate_wrapper @wraps_view @@ -631,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=False, accept=None, - header=None, path_info=None, custom_predicates=(), - context=None, decorator=None, mapper=None, http_cache=None, - match_param=None): + 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* @@ -660,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 @@ -686,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 @@ -971,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) @@ -1003,12 +1034,6 @@ class ViewsConfiguratorMixin(object): # GET implies HEAD too request_method = as_sorted_tuple(request_method + ('HEAD',)) - order, predicates, phash = make_predicates(xhr=xhr, - request_method=request_method, path_info=path_info, - request_param=request_param, header=header, accept=accept, - containment=containment, request_type=request_type, - match_param=match_param, custom=custom_predicates) - if context is None: context = for_ @@ -1024,17 +1049,38 @@ class ViewsConfiguratorMixin(object): registry = self.registry) introspectables = [] - discriminator = [ - 'view', context, name, request_type, IView, containment, - request_param, request_method, route_name, attr, - xhr, accept, header, path_info, match_param] - discriminator.extend(sorted([hash(x) for x in custom_predicates])) - discriminator = tuple(discriminator) + pvals = other_predicates + pvals.update( + dict( + xhr=xhr, + request_method=request_method, + path_info=path_info, + request_param=request_param, + header=header, + accept=accept, + containment=containment, + request_type=request_type, + match_param=match_param, + custom=predvalseq(custom_predicates), + ) + ) + + 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, @@ -1057,9 +1103,15 @@ class ViewsConfiguratorMixin(object): decorator=decorator, ) ) + view_intr.update(**other_predicates) introspectables.append(view_intr) + predlist = self.view_predlist def register(permission=permission, renderer=renderer): + # 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, @@ -1084,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=predicates, - 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 @@ -1203,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 @@ -1223,13 +1288,72 @@ 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) self.action(discriminator, register, introspectables=introspectables) + @property + def view_predlist(self): + predlist = self.registry.queryUtility(IPredicateList, name='view') + if predlist is None: + predlist = PredicateList() + self.registry.registerUtility(predlist, IPredicateList, name='view') + return predlist + + @action_method + def add_view_predicate(self, name, factory, weighs_more_than=None, + weighs_less_than=None): + """ 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`` by others). + + ``factory`` should be a :term:`predicate factory`. + """ + discriminator = ('view predicate', name) + intr = self.introspectable( + 'view predicates', + discriminator, + 'view predicate named %s' % name, + 'view 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.view_predlist + predlist.add(name, factory, weighs_more_than=weighs_more_than, + weighs_less_than=weighs_less_than) + self.action(discriminator, register, introspectables=(intr,), + order=PHASE1_CONFIG) # must be registered before views added + + def add_default_view_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), + ('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): """ Create a :term:`view callable` using the function, instance, diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 1445ee394..114a01854 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -1111,6 +1111,9 @@ class IJSONAdapter(Interface): into a JSON-serializable primitive. """ +class IPredicateList(Interface): + """ Interface representing a predicate list """ + # configuration phases: a lower phase number means the actions associated # with this phase will be executed earlier than those with later phase # numbers. The default phase number is 0, FTR. diff --git a/pyramid/scripts/proutes.py b/pyramid/scripts/proutes.py index 29ec9e72a..f64107d2b 100644 --- a/pyramid/scripts/proutes.py +++ b/pyramid/scripts/proutes.py @@ -15,10 +15,10 @@ class PRoutesCommand(object): route, the pattern of the route, and the view callable which will be invoked when the route is matched. - This command accepts one positional argument named "config_uri". It + This command accepts one positional argument named 'config_uri'. It specifies the PasteDeploy config file to use for the interactive - shell. The format is "inifile#name". If the name is left off, "main" - will be assumed. Example: "proutes myapp.ini". + shell. The format is 'inifile#name'. If the name is left off, 'main' + will be assumed. Example: 'proutes myapp.ini'. """ bootstrap = (bootstrap,) diff --git a/pyramid/scripts/pviews.py b/pyramid/scripts/pviews.py index 72a9800c3..a9db59dc1 100644 --- a/pyramid/scripts/pviews.py +++ b/pyramid/scripts/pviews.py @@ -17,11 +17,11 @@ class PViewsCommand(object): each route+predicate set, print each view that might match and its predicates. - This command accepts two positional arguments: "config_uri" specifies the + This command accepts two positional arguments: 'config_uri' specifies the PasteDeploy config file to use for the interactive shell. The format is - "inifile#name". If the name is left off, "main" will be assumed. "url" + 'inifile#name'. If the name is left off, 'main' will be assumed. 'url' specifies the path info portion of a URL that will be used to find - matching views. Example: "proutes myapp.ini#main /url" + matching views. Example: 'proutes myapp.ini#main /url' """ stdout = sys.stdout @@ -223,7 +223,7 @@ class PViewsCommand(object): self.out("%srequired permission = %s" % (indent, permission)) predicates = getattr(view_wrapper, '__predicates__', None) if predicates is not None: - predicate_text = ', '.join([p.__text__ for p in predicates]) + predicate_text = ', '.join([p.text() for p in predicates]) self.out("%sview predicates (%s)" % (indent, predicate_text)) def run(self): diff --git a/pyramid/testing.py b/pyramid/testing.py index 40e90cda6..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 @@ -824,6 +825,8 @@ def setUp(registry=None, request=None, hook_zca=True, autocommit=True, # ``render_template`` and friends went behind the back of # 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 37c3de275..abe22400b 100644 --- a/pyramid/tests/test_config/test_init.py +++ b/pyramid/tests/test_config/test_init.py @@ -349,7 +349,7 @@ class ConfiguratorTests(unittest.TestCase): config.setup_registry() self.assertEqual(reg.has_listeners, True) - def test_setup_registry_registers_default_exceptionresponse_view(self): + def test_setup_registry_registers_default_exceptionresponse_views(self): from webob.exc import WSGIHTTPException from pyramid.interfaces import IExceptionResponse from pyramid.view import default_exceptionresponse_view @@ -357,6 +357,7 @@ class ConfiguratorTests(unittest.TestCase): config = self._makeOne(reg) views = [] config.add_view = lambda *arg, **kw: views.append((arg, kw)) + config.add_default_view_predicates = lambda *arg: None config._add_tween = lambda *arg, **kw: False config.setup_registry() self.assertEqual(views[0], ((default_exceptionresponse_view,), @@ -364,6 +365,16 @@ class ConfiguratorTests(unittest.TestCase): self.assertEqual(views[1], ((default_exceptionresponse_view,), {'context':WSGIHTTPException})) + def test_setup_registry_registers_default_view_predicates(self): + reg = DummyRegistry() + config = self._makeOne(reg) + vp_called = [] + config.add_view = lambda *arg, **kw: None + config.add_default_view_predicates = lambda *arg: vp_called.append(True) + config._add_tween = lambda *arg, **kw: False + config.setup_registry() + self.assertTrue(vp_called) + def test_setup_registry_registers_default_webob_iresponse_adapter(self): from webob import Response from pyramid.interfaces import IResponse @@ -1385,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 @@ -1682,6 +1689,7 @@ class Test_resolveConflicts(unittest.TestCase): (3, f, (3,), {}, ('y',)), (None, f, (5,), {}, ('y',)), ]) + result = list(result) self.assertEqual( result, [{'info': None, @@ -1743,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, @@ -1794,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( @@ -1940,10 +1948,11 @@ class DummyEvent: pass class DummyRegistry(object): - def __init__(self, adaptation=None): + def __init__(self, adaptation=None, util=None): self.utilities = [] self.adapters = [] self.adaptation = adaptation + self.util = util def subscribers(self, events, name): self.events = events return events @@ -1953,6 +1962,8 @@ class DummyRegistry(object): self.adapters.append((arg, kw)) def queryAdapter(self, *arg, **kw): return self.adaptation + def queryUtility(self, *arg, **kw): + return self.util from pyramid.interfaces import IResponse @implementer(IResponse) diff --git a/pyramid/tests/test_config/test_predicates.py b/pyramid/tests/test_config/test_predicates.py new file mode 100644 index 000000000..94e613715 --- /dev/null +++ b/pyramid/tests/test_config/test_predicates.py @@ -0,0 +1,264 @@ +import unittest + +from pyramid.compat import text_ + +class TestXHRPredicate(unittest.TestCase): + def _makeOne(self, val): + from pyramid.config.predicates import XHRPredicate + return XHRPredicate(val, None) + + def test___call___true(self): + inst = self._makeOne(True) + request = Dummy() + request.is_xhr = True + result = inst(None, request) + self.assertTrue(result) + + def test___call___false(self): + inst = self._makeOne(True) + request = Dummy() + request.is_xhr = False + result = inst(None, request) + self.assertFalse(result) + + def test_text(self): + inst = self._makeOne(True) + self.assertEqual(inst.text(), 'xhr = True') + + def test_phash(self): + inst = self._makeOne(True) + self.assertEqual(inst.phash(), 'xhr = True') + +class TestRequestMethodPredicate(unittest.TestCase): + def _makeOne(self, val): + from pyramid.config.predicates import RequestMethodPredicate + return RequestMethodPredicate(val, None) + + def test___call___true_single(self): + inst = self._makeOne('GET') + request = Dummy() + request.method = 'GET' + result = inst(None, request) + self.assertTrue(result) + + def test___call___true_multi(self): + inst = self._makeOne(('GET','HEAD')) + request = Dummy() + request.method = 'GET' + result = inst(None, request) + self.assertTrue(result) + + def test___call___false(self): + inst = self._makeOne(('GET','HEAD')) + request = Dummy() + request.method = 'POST' + result = inst(None, request) + self.assertFalse(result) + + def test_text(self): + inst = self._makeOne(('HEAD','GET')) + self.assertEqual(inst.text(), 'request_method = GET,HEAD') + + def test_phash(self): + inst = self._makeOne(('HEAD','GET')) + self.assertEqual(inst.phash(), 'request_method = GET,HEAD') + +class TestPathInfoPredicate(unittest.TestCase): + def _makeOne(self, val): + from pyramid.config.predicates import PathInfoPredicate + return PathInfoPredicate(val, None) + + def test_ctor_compilefail(self): + from pyramid.exceptions import ConfigurationError + self.assertRaises(ConfigurationError, self._makeOne, '\\') + + def test___call___true(self): + inst = self._makeOne(r'/\d{2}') + request = Dummy() + request.upath_info = text_('/12') + result = inst(None, request) + self.assertTrue(result) + + def test___call___false(self): + inst = self._makeOne(r'/\d{2}') + request = Dummy() + request.upath_info = text_('/n12') + result = inst(None, request) + self.assertFalse(result) + + def test_text(self): + inst = self._makeOne('/') + self.assertEqual(inst.text(), 'path_info = /') + + def test_phash(self): + inst = self._makeOne('/') + self.assertEqual(inst.phash(), 'path_info = /') + +class TestRequestParamPredicate(unittest.TestCase): + def _makeOne(self, val): + from pyramid.config.predicates import RequestParamPredicate + return RequestParamPredicate(val, None) + + def test___call___true_exists(self): + inst = self._makeOne('abc') + request = Dummy() + request.params = {'abc':1} + result = inst(None, request) + self.assertTrue(result) + + def test___call___true_withval(self): + inst = self._makeOne('abc=1') + request = Dummy() + request.params = {'abc':'1'} + result = inst(None, request) + self.assertTrue(result) + + def test___call___false(self): + inst = self._makeOne('abc') + request = Dummy() + request.params = {} + result = inst(None, request) + self.assertFalse(result) + + def test_text_exists(self): + inst = self._makeOne('abc') + self.assertEqual(inst.text(), 'request_param abc') + + def test_text_withval(self): + inst = self._makeOne('abc= 1') + self.assertEqual(inst.text(), 'request_param abc = 1') + + def test_phash_exists(self): + inst = self._makeOne('abc') + self.assertEqual(inst.phash(), 'request_param abc') + + def test_phash_withval(self): + inst = self._makeOne('abc= 1') + self.assertEqual(inst.phash(), "request_param abc = 1") + +class TestMatchParamPredicate(unittest.TestCase): + def _makeOne(self, val): + from pyramid.config.predicates import MatchParamPredicate + return MatchParamPredicate(val, None) + + def test___call___true_single(self): + inst = self._makeOne('abc=1') + request = Dummy() + request.matchdict = {'abc':'1'} + result = inst(None, request) + self.assertTrue(result) + + + def test___call___true_multi(self): + inst = self._makeOne(('abc=1', 'def=2')) + request = Dummy() + request.matchdict = {'abc':'1', 'def':'2'} + result = inst(None, request) + self.assertTrue(result) + + def test___call___false(self): + inst = self._makeOne('abc=1') + request = Dummy() + request.matchdict = {} + result = inst(None, request) + self.assertFalse(result) + + def test_text(self): + inst = self._makeOne(('def= 1', 'abc =2')) + self.assertEqual(inst.text(), 'match_param abc=2,def=1') + + def test_phash(self): + inst = self._makeOne(('def= 1', 'abc =2')) + self.assertEqual(inst.phash(), 'match_param abc=2,def=1') + +class TestCustomPredicate(unittest.TestCase): + def _makeOne(self, val): + from pyramid.config.predicates import CustomPredicate + return CustomPredicate(val, None) + + def test___call___true(self): + def func(context, request): + self.assertEqual(context, None) + self.assertEqual(request, None) + return True + inst = self._makeOne(func) + result = inst(None, None) + self.assertTrue(result) + + def test___call___false(self): + def func(context, request): + self.assertEqual(context, None) + self.assertEqual(request, None) + return False + inst = self._makeOne(func) + result = inst(None, None) + self.assertFalse(result) + + def test_text_func_has___text__(self): + pred = predicate() + pred.__text__ = 'text' + inst = self._makeOne(pred) + self.assertEqual(inst.text(), 'text') + + def test_text_func_repr(self): + pred = predicate() + inst = self._makeOne(pred) + self.assertEqual(inst.text(), 'custom predicate: object predicate') + + def test_phash(self): + pred = predicate() + inst = self._makeOne(pred) + self.assertEqual(inst.phash(), 'custom:1') + +class TestTraversePredicate(unittest.TestCase): + def _makeOne(self, val): + from pyramid.config.predicates import TraversePredicate + return TraversePredicate(val, None) + + def test___call__traverse_has_remainder_already(self): + inst = self._makeOne('/1/:a/:b') + info = {'traverse':'abc'} + request = Dummy() + result = inst(info, request) + self.assertEqual(result, True) + self.assertEqual(info, {'traverse':'abc'}) + + def test___call__traverse_matches(self): + inst = self._makeOne('/1/:a/:b') + info = {'match':{'a':'a', 'b':'b'}} + request = Dummy() + result = inst(info, request) + self.assertEqual(result, True) + self.assertEqual(info, {'match': + {'a':'a', 'b':'b', 'traverse':('1', 'a', 'b')}}) + + def test___call__traverse_matches_with_highorder_chars(self): + inst = self._makeOne(text_(b'/La Pe\xc3\xb1a/{x}', 'utf-8')) + info = {'match':{'x':text_(b'Qu\xc3\xa9bec', 'utf-8')}} + request = Dummy() + result = inst(info, request) + self.assertEqual(result, True) + self.assertEqual( + info['match']['traverse'], + (text_(b'La Pe\xc3\xb1a', 'utf-8'), + text_(b'Qu\xc3\xa9bec', 'utf-8')) + ) + + def test_text(self): + inst = self._makeOne('/abc') + self.assertEqual(inst.text(), 'traverse matchdict pseudo-predicate') + + def test_phash(self): + inst = self._makeOne('/abc') + self.assertEqual(inst.phash(), '') + +class predicate(object): + def __repr__(self): + return 'predicate' + def __hash__(self): + return 1 + +class Dummy(object): + def __init__(self, **kw): + self.__dict__.update(**kw) + 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_tweens.py b/pyramid/tests/test_config/test_tweens.py index 0d96bf601..8853b6899 100644 --- a/pyramid/tests/test_config/test_tweens.py +++ b/pyramid/tests/test_config/test_tweens.py @@ -179,28 +179,12 @@ class TestTweens(unittest.TestCase): ('name2', 'factory2')]) def test_add_implicit(self): - from pyramid.tweens import INGRESS tweens = self._makeOne() tweens.add_implicit('name', 'factory') - self.assertEqual(tweens.names, ['name']) - self.assertEqual(tweens.factories, - {'name':'factory'}) - self.assertEqual(tweens.order, [(INGRESS, 'name')]) tweens.add_implicit('name2', 'factory2') - self.assertEqual(tweens.names, ['name', 'name2']) - self.assertEqual(tweens.factories, - {'name':'factory', 'name2':'factory2'}) - self.assertEqual(tweens.order, - [(INGRESS, 'name'), (INGRESS, 'name2')]) - tweens.add_implicit('name3', 'factory3', over='name2') - self.assertEqual(tweens.names, - ['name', 'name2', 'name3']) - self.assertEqual(tweens.factories, - {'name':'factory', 'name2':'factory2', - 'name3':'factory3'}) - self.assertEqual(tweens.order, - [(INGRESS, 'name'), (INGRESS, 'name2'), - ('name3', 'name2')]) + self.assertEqual(tweens.sorter.sorted(), + [('name2', 'factory2'), + ('name', 'factory')]) def test___call___explicit(self): tweens = self._makeOne() @@ -212,18 +196,13 @@ class TestTweens(unittest.TestCase): self.assertEqual(tweens(None, None), '123') def test___call___implicit(self): - from pyramid.tweens import INGRESS tweens = self._makeOne() def factory1(handler, registry): return handler def factory2(handler, registry): return '123' - tweens.names = ['name', 'name2'] - tweens.alias_to_name = {'name':'name', 'name2':'name2'} - tweens.name_to_alias = {'name':'name', 'name2':'name2'} - tweens.req_under = set(['name', 'name2']) - tweens.order = [(INGRESS, 'name'), (INGRESS, 'name2')] - tweens.factories = {'name':factory1, 'name2':factory2} + tweens.add_implicit('name2', factory2) + tweens.add_implicit('name1', factory1) self.assertEqual(tweens(None, None), '123') def test_implicit_ordering_1(self): @@ -413,7 +392,7 @@ class TestTweens(unittest.TestCase): self.assertRaises(ConfigurationError, tweens.implicit) def test_implicit_ordering_conflict_direct(self): - from pyramid.config.tweens import CyclicDependencyError + from pyramid.config.util import CyclicDependencyError tweens = self._makeOne() add = tweens.add_implicit add('browserid', 'browserid_factory') @@ -421,7 +400,7 @@ class TestTweens(unittest.TestCase): self.assertRaises(CyclicDependencyError, tweens.implicit) def test_implicit_ordering_conflict_indirect(self): - from pyramid.config.tweens import CyclicDependencyError + from pyramid.config.util import CyclicDependencyError tweens = self._makeOne() add = tweens.add_implicit add('browserid', 'browserid_factory') @@ -429,14 +408,3 @@ class TestTweens(unittest.TestCase): add('dbt', 'dbt_factory', under='browserid', over='auth') self.assertRaises(CyclicDependencyError, tweens.implicit) -class TestCyclicDependencyError(unittest.TestCase): - def _makeOne(self, cycles): - from pyramid.config.tweens import CyclicDependencyError - return CyclicDependencyError(cycles) - - def test___str__(self): - exc = self._makeOne({'a':['c', 'd'], 'c':['a']}) - result = str(exc) - self.assertTrue("'a' sorts over ['c', 'd']" in result) - self.assertTrue("'c' sorts over ['a']" in result) - diff --git a/pyramid/tests/test_config/test_util.py b/pyramid/tests/test_config/test_util.py index 1ad1fb3c1..13cb27526 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__, '<unknown custom predicate>') + 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') @@ -328,15 +361,10 @@ class Test__make_predicates(unittest.TestCase): hash2, _, __= self._callFUT(request_method='GET') self.assertEqual(hash1, hash2) - def test_match_param_hashable(self): - # https://github.com/Pylons/pyramid/issues/425 - import pyramid.testing - def view(request): pass - config = pyramid.testing.setUp(autocommit=False) - config.add_route('foo', '/foo/{a}/{b}') - config.add_view(view, route_name='foo', match_param='a=bar') - config.add_view(view, route_name='foo', match_param=('a=bar', 'b=baz')) - config.commit() + def test_unknown_predicate(self): + from pyramid.exceptions import ConfigurationError + self.assertRaises(ConfigurationError, self._callFUT, unknown=1) + class TestActionInfo(unittest.TestCase): def _getTargetClass(self): @@ -368,6 +396,274 @@ class TestActionInfo(unittest.TestCase): self.assertEqual(str(inst), "Line 0 of file filename:\n linerepr ") +class TestTopologicalSorter(unittest.TestCase): + def _makeOne(self, *arg, **kw): + from pyramid.config.util import TopologicalSorter + return TopologicalSorter(*arg, **kw) + + def test_remove(self): + inst = self._makeOne() + inst.names.append('name') + inst.name2val['name'] = 1 + inst.req_after.add('name') + inst.req_before.add('name') + inst.name2after['name'] = ('bob',) + inst.name2before['name'] = ('fred',) + inst.order.append(('bob', 'name')) + inst.order.append(('name', 'fred')) + inst.remove('name') + self.assertFalse(inst.names) + self.assertFalse(inst.req_before) + self.assertFalse(inst.req_after) + self.assertFalse(inst.name2before) + self.assertFalse(inst.name2after) + self.assertFalse(inst.name2val) + self.assertFalse(inst.order) + + def test_add(self): + from pyramid.config.util import LAST + sorter = self._makeOne() + sorter.add('name', 'factory') + self.assertEqual(sorter.names, ['name']) + self.assertEqual(sorter.name2val, + {'name':'factory'}) + self.assertEqual(sorter.order, [('name', LAST)]) + sorter.add('name2', 'factory2') + self.assertEqual(sorter.names, ['name', 'name2']) + self.assertEqual(sorter.name2val, + {'name':'factory', 'name2':'factory2'}) + self.assertEqual(sorter.order, + [('name', LAST), ('name2', LAST)]) + sorter.add('name3', 'factory3', before='name2') + self.assertEqual(sorter.names, + ['name', 'name2', 'name3']) + self.assertEqual(sorter.name2val, + {'name':'factory', 'name2':'factory2', + 'name3':'factory3'}) + self.assertEqual(sorter.order, + [('name', LAST), ('name2', LAST), + ('name3', 'name2')]) + + def test_sorted_ordering_1(self): + sorter = self._makeOne() + sorter.add('name1', 'factory1') + sorter.add('name2', 'factory2') + self.assertEqual(sorter.sorted(), + [ + ('name1', 'factory1'), + ('name2', 'factory2'), + ]) + + def test_sorted_ordering_2(self): + from pyramid.config.util import FIRST + sorter = self._makeOne() + sorter.add('name1', 'factory1') + sorter.add('name2', 'factory2', after=FIRST) + self.assertEqual(sorter.sorted(), + [ + ('name2', 'factory2'), + ('name1', 'factory1'), + ]) + + def test_sorted_ordering_3(self): + from pyramid.config.util import FIRST + sorter = self._makeOne() + add = sorter.add + add('auth', 'auth_factory', after='browserid') + add('dbt', 'dbt_factory') + add('retry', 'retry_factory', before='txnmgr', after='exceptionview') + add('browserid', 'browserid_factory') + add('txnmgr', 'txnmgr_factory', after='exceptionview') + add('exceptionview', 'excview_factory', after=FIRST) + self.assertEqual(sorter.sorted(), + [ + ('exceptionview', 'excview_factory'), + ('retry', 'retry_factory'), + ('txnmgr', 'txnmgr_factory'), + ('dbt', 'dbt_factory'), + ('browserid', 'browserid_factory'), + ('auth', 'auth_factory'), + ]) + + def test_sorted_ordering_4(self): + from pyramid.config.util import FIRST + sorter = self._makeOne() + add = sorter.add + add('exceptionview', 'excview_factory', after=FIRST) + add('auth', 'auth_factory', after='browserid') + add('retry', 'retry_factory', before='txnmgr', after='exceptionview') + add('browserid', 'browserid_factory') + add('txnmgr', 'txnmgr_factory', after='exceptionview') + add('dbt', 'dbt_factory') + self.assertEqual(sorter.sorted(), + [ + ('exceptionview', 'excview_factory'), + ('retry', 'retry_factory'), + ('txnmgr', 'txnmgr_factory'), + ('browserid', 'browserid_factory'), + ('auth', 'auth_factory'), + ('dbt', 'dbt_factory'), + ]) + + def test_sorted_ordering_5(self): + from pyramid.config.util import LAST, FIRST + sorter = self._makeOne() + add = sorter.add + add('exceptionview', 'excview_factory') + add('auth', 'auth_factory', after=FIRST) + add('retry', 'retry_factory', before='txnmgr', after='exceptionview') + add('browserid', 'browserid_factory', after=FIRST) + add('txnmgr', 'txnmgr_factory', after='exceptionview', before=LAST) + add('dbt', 'dbt_factory') + self.assertEqual(sorter.sorted(), + [ + ('browserid', 'browserid_factory'), + ('auth', 'auth_factory'), + ('exceptionview', 'excview_factory'), + ('retry', 'retry_factory'), + ('txnmgr', 'txnmgr_factory'), + ('dbt', 'dbt_factory'), + ]) + + def test_sorted_ordering_missing_before_partial(self): + from pyramid.exceptions import ConfigurationError + sorter = self._makeOne() + add = sorter.add + add('dbt', 'dbt_factory') + add('auth', 'auth_factory', after='browserid') + add('retry', 'retry_factory', before='txnmgr', after='exceptionview') + add('browserid', 'browserid_factory') + self.assertRaises(ConfigurationError, sorter.sorted) + + def test_sorted_ordering_missing_after_partial(self): + from pyramid.exceptions import ConfigurationError + sorter = self._makeOne() + add = sorter.add + add('dbt', 'dbt_factory') + add('auth', 'auth_factory', after='txnmgr') + add('retry', 'retry_factory', before='dbt', after='exceptionview') + add('browserid', 'browserid_factory') + self.assertRaises(ConfigurationError, sorter.sorted) + + def test_sorted_ordering_missing_before_and_after_partials(self): + from pyramid.exceptions import ConfigurationError + sorter = self._makeOne() + add = sorter.add + add('dbt', 'dbt_factory') + add('auth', 'auth_factory', after='browserid') + add('retry', 'retry_factory', before='foo', after='txnmgr') + add('browserid', 'browserid_factory') + self.assertRaises(ConfigurationError, sorter.sorted) + + def test_sorted_ordering_missing_before_partial_with_fallback(self): + from pyramid.config.util import LAST + sorter = self._makeOne() + add = sorter.add + add('exceptionview', 'excview_factory', before=LAST) + add('auth', 'auth_factory', after='browserid') + add('retry', 'retry_factory', before=('txnmgr', LAST), + after='exceptionview') + add('browserid', 'browserid_factory') + add('dbt', 'dbt_factory') + self.assertEqual(sorter.sorted(), + [ + ('exceptionview', 'excview_factory'), + ('retry', 'retry_factory'), + ('browserid', 'browserid_factory'), + ('auth', 'auth_factory'), + ('dbt', 'dbt_factory'), + ]) + + def test_sorted_ordering_missing_after_partial_with_fallback(self): + from pyramid.config.util import FIRST + sorter = self._makeOne() + add = sorter.add + add('exceptionview', 'excview_factory', after=FIRST) + add('auth', 'auth_factory', after=('txnmgr','browserid')) + add('retry', 'retry_factory', after='exceptionview') + add('browserid', 'browserid_factory') + add('dbt', 'dbt_factory') + self.assertEqual(sorter.sorted(), + [ + ('exceptionview', 'excview_factory'), + ('retry', 'retry_factory'), + ('browserid', 'browserid_factory'), + ('auth', 'auth_factory'), + ('dbt', 'dbt_factory'), + ]) + + def test_sorted_ordering_with_partial_fallbacks(self): + from pyramid.config.util import LAST + sorter = self._makeOne() + add = sorter.add + add('exceptionview', 'excview_factory', before=('wontbethere', LAST)) + add('retry', 'retry_factory', after='exceptionview') + add('browserid', 'browserid_factory', before=('wont2', 'exceptionview')) + self.assertEqual(sorter.sorted(), + [ + ('browserid', 'browserid_factory'), + ('exceptionview', 'excview_factory'), + ('retry', 'retry_factory'), + ]) + + def test_sorted_ordering_with_multiple_matching_fallbacks(self): + from pyramid.config.util import LAST + sorter = self._makeOne() + add = sorter.add + add('exceptionview', 'excview_factory', before=LAST) + add('retry', 'retry_factory', after='exceptionview') + add('browserid', 'browserid_factory', before=('retry', 'exceptionview')) + self.assertEqual(sorter.sorted(), + [ + ('browserid', 'browserid_factory'), + ('exceptionview', 'excview_factory'), + ('retry', 'retry_factory'), + ]) + + def test_sorted_ordering_with_missing_fallbacks(self): + from pyramid.exceptions import ConfigurationError + from pyramid.config.util import LAST + sorter = self._makeOne() + add = sorter.add + add('exceptionview', 'excview_factory', before=LAST) + add('retry', 'retry_factory', after='exceptionview') + add('browserid', 'browserid_factory', before=('txnmgr', 'auth')) + self.assertRaises(ConfigurationError, sorter.sorted) + + def test_sorted_ordering_conflict_direct(self): + from pyramid.config.util import CyclicDependencyError + sorter = self._makeOne() + add = sorter.add + add('browserid', 'browserid_factory') + add('auth', 'auth_factory', before='browserid', after='browserid') + self.assertRaises(CyclicDependencyError, sorter.sorted) + + def test_sorted_ordering_conflict_indirect(self): + from pyramid.config.util import CyclicDependencyError + sorter = self._makeOne() + add = sorter.add + add('browserid', 'browserid_factory') + add('auth', 'auth_factory', before='browserid') + add('dbt', 'dbt_factory', after='browserid', before='auth') + self.assertRaises(CyclicDependencyError, sorter.sorted) + +class TestSingleton(unittest.TestCase): + def test_repr(self): + from pyramid.config.util import Singleton + r = repr(Singleton('ABC')) + self.assertEqual(r, 'ABC') + +class TestCyclicDependencyError(unittest.TestCase): + def _makeOne(self, cycles): + from pyramid.config.util import CyclicDependencyError + return CyclicDependencyError(cycles) + + def test___str__(self): + exc = self._makeOne({'a':['c', 'd'], 'c':['a']}) + result = str(exc) + self.assertTrue("'a' sorts before ['c', 'd']" in result) + self.assertTrue("'c' sorts before ['a']" in result) + class DummyCustomPredicate(object): def __init__(self): self.__text__ = 'custom predicate' @@ -392,3 +688,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 ebf1dfb39..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 @@ -400,7 +401,7 @@ class TestViewsConfigurationMixin(unittest.TestCase): from pyramid.interfaces import IViewClassifier from pyramid.interfaces import IMultiView phash = md5() - phash.update(b'xhr:True') + phash.update(b'xhr = True') view = lambda *arg: 'NOT OK' view.__phash__ = phash.hexdigest() config = self._makeOne(autocommit=True) @@ -424,7 +425,7 @@ class TestViewsConfigurationMixin(unittest.TestCase): from pyramid.interfaces import IExceptionViewClassifier from pyramid.interfaces import IMultiView phash = md5() - phash.update(b'xhr:True') + phash.update(b'xhr = True') view = lambda *arg: 'NOT OK' view.__phash__ = phash.hexdigest() config = self._makeOne(autocommit=True) @@ -970,8 +971,10 @@ class TestViewsConfigurationMixin(unittest.TestCase): wrapper = self._getViewCallable(config) self.assertTrue(IMultiView.providedBy(wrapper)) request = self._makeRequest(config) - self.assertEqual(wrapper.__discriminator__(foo, request)[5], IFoo) - self.assertEqual(wrapper.__discriminator__(bar, request)[5], IBar) + self.assertNotEqual( + wrapper.__discriminator__(foo, request), + wrapper.__discriminator__(bar, request), + ) def test_add_view_with_template_renderer(self): from pyramid.tests import test_config @@ -1217,8 +1220,8 @@ class TestViewsConfigurationMixin(unittest.TestCase): def test_add_view_with_header_badregex(self): view = lambda *arg: 'OK' config = self._makeOne() - self.assertRaises(ConfigurationError, - config.add_view, view=view, header='Host:a\\') + config.add_view(view, header='Host:a\\') + self.assertRaises(ConfigurationError, config.commit) def test_add_view_with_header_noval_match(self): from pyramid.renderers import null_renderer @@ -1323,8 +1326,8 @@ class TestViewsConfigurationMixin(unittest.TestCase): def test_add_view_with_path_info_badregex(self): view = lambda *arg: 'OK' config = self._makeOne() - self.assertRaises(ConfigurationError, - config.add_view, view=view, path_info='\\') + config.add_view(view, path_info='\\') + self.assertRaises(ConfigurationError, config.commit) def test_add_view_with_path_info_match(self): from pyramid.renderers import null_renderer @@ -2905,7 +2908,7 @@ class TestViewDeriver(unittest.TestCase): view = lambda *arg: response def predicate1(context, request): return False - predicate1.__text__ = 'text' + predicate1.text = lambda *arg: 'text' deriver = self._makeOne(predicates=[predicate1]) result = deriver(view) request = self._makeRequest() @@ -2923,7 +2926,7 @@ class TestViewDeriver(unittest.TestCase): def myview(request): pass def predicate1(context, request): return False - predicate1.__text__ = 'text' + predicate1.text = lambda *arg: 'text' deriver = self._makeOne(predicates=[predicate1]) result = deriver(myview) request = self._makeRequest() @@ -2941,10 +2944,10 @@ class TestViewDeriver(unittest.TestCase): def myview(request): pass def predicate1(context, request): return True - predicate1.__text__ = 'pred1' + predicate1.text = lambda *arg: 'pred1' def predicate2(context, request): return False - predicate2.__text__ = 'pred2' + predicate2.text = lambda *arg: 'pred2' deriver = self._makeOne(predicates=[predicate1, predicate2]) result = deriver(myview) request = self._makeRequest() @@ -2999,11 +3002,11 @@ class TestViewDeriver(unittest.TestCase): def predicate1(context, request): predicates.append(True) return True - predicate1.__text__ = 'text' + predicate1.text = lambda *arg: 'text' def predicate2(context, request): predicates.append(True) return False - predicate2.__text__ = 'text' + predicate2.text = lambda *arg: 'text' deriver = self._makeOne(predicates=[predicate1, predicate2]) result = deriver(view) request = self._makeRequest() diff --git a/pyramid/tests/test_scaffolds/test_copydir.py b/pyramid/tests/test_scaffolds/test_copydir.py index 68cefbe6e..d757b837c 100644 --- a/pyramid/tests/test_scaffolds/test_copydir.py +++ b/pyramid/tests/test_scaffolds/test_copydir.py @@ -176,6 +176,14 @@ class Test_makedirs(unittest.TestCase): self._callFUT(target, 2, None) shutil.rmtree(tmpdir) + def test_makedirs_no_parent_dir(self): + import shutil + import tempfile + tmpdir = tempfile.mkdtemp() + target = os.path.join(tmpdir, 'nonexistent_subdir', 'non2') + self._callFUT(target, 2, None) + shutil.rmtree(tmpdir) + class Test_support_functions(unittest.TestCase): def _call_html_quote(self, *arg, **kw): diff --git a/pyramid/tests/test_scripts/test_pviews.py b/pyramid/tests/test_scripts/test_pviews.py index 680d48cee..6a919c31b 100644 --- a/pyramid/tests/test_scripts/test_pviews.py +++ b/pyramid/tests/test_scripts/test_pviews.py @@ -309,7 +309,7 @@ class TestPViewsCommand(unittest.TestCase): L = [] command.out = L.append def predicate(): pass - predicate.__text__ = "predicate = x" + predicate.text = lambda *arg: "predicate = x" view = dummy.DummyView(context='context', view_name='a') view.__predicates__ = [predicate] command._find_view = lambda arg1, arg2: view @@ -448,7 +448,7 @@ class TestPViewsCommand(unittest.TestCase): L = [] command.out = L.append def predicate(): pass - predicate.__text__ = "predicate = x" + predicate.text = lambda *arg: "predicate = x" view = dummy.DummyView(context='context') view.__name__ = 'view' view.__view_attr__ = 'call' 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') diff --git a/pyramid/tests/test_url.py b/pyramid/tests/test_url.py index 50deb63f3..a7a565356 100644 --- a/pyramid/tests/test_url.py +++ b/pyramid/tests/test_url.py @@ -2,10 +2,8 @@ import os import unittest import warnings -from pyramid.testing import ( - setUp, - tearDown, - ) +from pyramid import testing + from pyramid.compat import ( text_, native_, @@ -14,10 +12,10 @@ from pyramid.compat import ( class TestURLMethodsMixin(unittest.TestCase): def setUp(self): - self.config = setUp() + self.config = testing.setUp() def tearDown(self): - tearDown() + testing.tearDown() def _makeOne(self, environ=None): from pyramid.url import URLMethodsMixin diff --git a/pyramid/tests/test_view.py b/pyramid/tests/test_view.py index a105adb70..ee4994172 100644 --- a/pyramid/tests/test_view.py +++ b/pyramid/tests/test_view.py @@ -3,17 +3,14 @@ import sys from zope.interface import implementer -from pyramid.testing import ( - setUp, - tearDown, - ) +from pyramid import testing class BaseTest(object): def setUp(self): - self.config = setUp() + self.config = testing.setUp() def tearDown(self): - tearDown() + testing.tearDown() def _registerView(self, reg, app, name): from pyramid.interfaces import IRequest @@ -334,10 +331,10 @@ class TestIsResponse(unittest.TestCase): class TestViewConfigDecorator(unittest.TestCase): def setUp(self): - setUp() + testing.setUp() def tearDown(self): - tearDown() + testing.tearDown() def _getTargetClass(self): from pyramid.view import view_config |
