Anonymous avatar Anonymous committed cbdeff3

heavily improved local objects. Should pick up standalone greenlet
builds now and support proxies to free callables as well. There is
also a stacked local now that makes it possible to invoke the same
application from within itself by pushing current request/response
on top of the stack.

routing build method will also build non-default method rules properly
if no method is provided.

Comments (0)

Files changed (6)

 -------------
 (bugfix release, release date to be decided)
 
+- heavily improved local objects.  Should pick up standalone greenlet
+  builds now and support proxies to free callables as well.  There is
+  also a stacked local now that makes it possible to invoke the same
+  application from within itself by pushing current request/response
+  on top of the stack.
+- routing build method will also build non-default method rules properly
+  if no method is provided.
 - added proper IPv6 support for the builtin server.
 - windows specific filesystem session store fixes.
   (should no be more stable under high concurrency)

tests/test_local.py

 
 from nose.tools import assert_raises
 
-from werkzeug import Local, LocalManager
+from werkzeug import Local, LocalManager, LocalStack, LocalProxy, release_local
 
 
 def test_basic_local():
     delfoo()
     assert_raises(AttributeError, lambda: l.foo)
     assert_raises(AttributeError, delfoo)
+
+    release_local(l)
+
+
+def test_local_release():
+    """Locals work without manager"""
+    loc = Local()
+    loc.foo = 42
+    release_local(loc)
+    assert not hasattr(loc, 'foo')
+
+    ls = LocalStack()
+    ls.push(42)
+    release_local(ls)
+    assert ls.top is None
+
+
+def test_local_stack():
+    """Test the LocalStack"""
+    ls = LocalStack()
+    assert ls.top is None
+    ls.push(42)
+    assert ls.top == 42
+    ls.push(23)
+    assert ls.top == 23
+    ls.pop()
+    assert ls.top == 42
+    ls.pop()
+    assert ls.top is None
+    assert ls.pop() is None
+    assert ls.pop() is None
+
+    proxy = ls()
+    ls.push([1, 2])
+    assert proxy == [1, 2]
+    ls.push((1, 2))
+    assert proxy == (1, 2)
+    ls.pop()
+    ls.pop()
+    assert repr(proxy) == '<LocalProxy unbound>'
+
+    release_local(ls)
+
+
+def test_local_proxies_with_callables():
+    """Use a callable with a local proxy"""
+    foo = 42
+    ls = LocalProxy(lambda: foo)
+    assert ls == 42
+    foo = [23]
+    ls.append(42)
+    assert ls == [23, 42]
+    assert foo == [23, 42]

tests/test_routing.py

         'http://example.org/bar/0.815?bif=1.0'
     assert adapter.build('barf', {'bazf': 0.815, 'bif' : 1.0},
         append_unknown=False) == 'http://example.org/bar/0.815'
+
+
+def test_method_fallback():
+    """Test that building falls back to different rules"""
+    map = Map([
+        Rule('/', endpoint='index', methods=['GET']),
+        Rule('/<name>', endpoint='hello_name', methods=['GET']),
+        Rule('/select', endpoint='hello_select', methods=['POST']),
+        Rule('/search_get', endpoint='search', methods=['GET']),
+        Rule('/search_post', endpoint='search', methods=['POST'])
+    ])
+    adapter = map.bind('example.com')
+    assert adapter.build('index') == '/'
+    assert adapter.build('index', method='GET') == '/'
+    assert adapter.build('hello_name', {'name': 'foo'}) == '/foo'
+    assert adapter.build('hello_select') == '/select'
+    assert adapter.build('hello_select', method='POST') == '/select'
+    assert adapter.build('search') == '/search_get'
+    assert adapter.build('search', method='GET') == '/search_get'
+    assert adapter.build('search', method='POST') == '/search_post'

werkzeug/__init__.py

 # import mapping to objects in other modules
 all_by_module = {
     'werkzeug.debug':       ['DebuggedApplication'],
-    'werkzeug.local':       ['Local', 'LocalManager', 'LocalProxy'],
+    'werkzeug.local':       ['Local', 'LocalManager', 'LocalProxy',
+                             'LocalStack', 'release_local'],
     'werkzeug.templates':   ['Template'],
     'werkzeug.serving':     ['run_simple'],
     'werkzeug.test':        ['Client', 'EnvironBuilder', 'create_environ',

werkzeug/local.py

     :license: BSD, see LICENSE for more details.
 """
 try:
-    from py.magic import greenlet
-    get_current_greenlet = greenlet.getcurrent
-    del greenlet
-except: # pragma: no cover
-    # catch all, py.* fails with so many different errors.
-    get_current_greenlet = int
+    from greenlet import getcurrent as get_current_greenlet
+except ImportError: # pragma: no cover
+    try:
+        from py.magic import greenlet
+        get_current_greenlet = greenlet.getcurrent
+        del greenlet
+    except:
+        # catch all, py.* fails with so many different errors.
+        get_current_greenlet = int
 try:
     from thread import get_ident as get_current_thread, allocate_lock
 except ImportError: # pragma: no cover
     get_ident = lambda: (get_current_thread(), get_current_greenlet())
 
 
+def release_local(local):
+    """Releases the contents of the local for the current context.
+    This makes it possible to use locals without a manager.
+
+    Example::
+
+        >>> loc = Local()
+        >>> loc.foo = 42
+        >>> release_local(loc)
+        >>> hasattr(loc, 'foo')
+        False
+
+    With this function one can release :class:`Local` objects as well
+    as :class:`StackLocal` objects.  However it is not possible to
+    release data held by proxies that way, one always has to retain
+    a reference to the underlying local object in order to be able
+    to release it.
+
+    .. versionadded:: 0.6.1
+    """
+    local.__release_local__()
+
+
 class Local(object):
     __slots__ = ('__storage__', '__lock__')
 
         """Create a proxy for a name."""
         return LocalProxy(self, proxy)
 
+    def __release_local__(self):
+        self.__storage__.pop(get_ident(), None)
+
     def __getattr__(self, name):
         self.__lock__.acquire()
         try:
             self.__lock__.release()
 
 
+class LocalStack(object):
+    """This class works similar to a :class:`Local` but keeps a stack
+    of objects instead.  This is best explained with an example::
+
+        >>> ls = LocalStack()
+        >>> ls.push(42)
+        >>> ls.top
+        42
+        >>> ls.push(23)
+        >>> ls.top
+        23
+        >>> ls.pop()
+        23
+        >>> ls.top
+        42
+
+    They can be release by using a :class:`LocalManager` or with the
+    :func:`release_local` function.  By calling the stack without
+    arguments it returns a proxy that resolves to the topmost item
+    on the stack.
+
+    .. versionadded:: 0.6.1
+    """
+
+    def __init__(self):
+        self._local = Local()
+        self._lock = allocate_lock()
+
+    def __release_local__(self):
+        self._local.__release_local__()
+
+    def __call__(self):
+        def _lookup():
+            rv = self.top
+            if rv is None:
+                raise RuntimeError('object unbound')
+            return rv
+        return LocalProxy(_lookup)
+
+    @property
+    def stack(self):
+        self._lock.acquire()
+        try:
+            rv = getattr(self._local, 'stack', None)
+            if rv is None:
+                self._local.stack = rv = []
+            return rv
+        finally:
+            self._lock.release()
+
+    def push(self, obj):
+        self.stack.append(obj)
+
+    def pop(self):
+        try:
+            return self.stack.pop()
+        except IndexError:
+            return None
+
+    @property
+    def top(self):
+        try:
+            return self.stack[-1]
+        except IndexError:
+            return None
+
+
 class LocalManager(object):
     """Local objects cannot manage themselves. For that you need a local
     manager.  You can pass a local manager multiple locals or add them later
     by appending them to `manager.locals`.  Everytime the manager cleans up
     it, will clean up all the data left in the locals for this context.
+
+    .. versionchanged:: 0.6.1
+       Instead of a manager the :func:`release_local` function can be used
+       as well.
     """
 
     def __init__(self, locals=None):
         """
         ident = self.get_ident()
         for local in self.locals:
-            local.__storage__.pop(ident, None)
+            release_local(local)
 
     def make_middleware(self, app):
         """Wrap a WSGI application so that cleaning up happens after
 
         from werkzeug import Local
         l = Local()
+
+        # these are proxies
         request = l('request')
         user = l('user')
 
+
+        from werkzeug import LocalStack
+        _response_local = LocalStack()
+
+        # this is a proxy
+        response = _response_local()
+
     Whenever something is bound to l.user / l.request the proxy objects
-    will forward all operations.  If no object is bound a `RuntimeError`
+    will forward all operations.  If no object is bound a :exc:`RuntimeError`
     will be raised.
+
+    To create proxies to :class:`Local` or :class:`LocalStack` objects,
+    call the object as shown above.  If you want to have a proxy to an
+    object looked up by a function, you can (as of Werkzeug 0.6.1) pass
+    a function to the :class:`LocalProxy` constructor::
+
+        session = LocalProxy(lambda: get_current_request().session)
+
+    .. versionchanged:: 0.6.1
+       The class can be instanciated with a callable as well now.
     """
     __slots__ = ('__local', '__dict__', '__name__')
 
-    def __init__(self, local, name):
+    def __init__(self, local, name=None):
         object.__setattr__(self, '_LocalProxy__local', local)
         object.__setattr__(self, '__name__', name)
 
         object behind the proxy at a time for performance reasons or because
         you want to pass the object into a different context.
         """
+        if not hasattr(self.__local, '__release_local__'):
+            return self.__local()
         try:
             return getattr(self.__local, self.__name__)
         except AttributeError:

werkzeug/routing.py

         subdomain, url = (u''.join(tmp)).split('|', 1)
 
         if append_unknown:
-            query_vars = {}
-            for key in set(values) - processed:
-                query_vars[key] = unicode(values[key])
+            query_vars = MultiDict(values)
+            for key in processed:
+                if key in query_vars:
+                    del query_vars[key]
+
             if query_vars:
                 url += '?' + url_encode(query_vars, self.map.charset,
                                         sort=self.map.sort_parameters,
                self.endpoint == rule.endpoint and self != rule and \
                self.arguments == rule.arguments
 
-    def suitable_for(self, values, method):
+    def suitable_for(self, values, method=None):
         """Check if the dict of values has enough data for url generation.
 
         :internal:
         """
-        if self.methods is not None and method not in self.methods:
-            return False
+        if method is not None:
+            if self.methods is not None and method not in self.methods:
+                return False
 
         valueset = set(values)
 
             return False
         return True
 
+    def _partial_build(self, endpoint, values, method, append_unknown):
+        """Helper for :meth:`build`.  Returns subdomain and path for the
+        rule that accepts this endpoint, values and method.
+
+        :internal:
+        """
+        # in case the method is none, try with the default method first
+        if method is None:
+            rv = self._partial_build(endpoint, values, self.default_method,
+                                     append_unknown)
+            if rv is not None:
+                return rv
+
+        # default method did not match or a specific method is passed,
+        # check all and go with first result.
+        for rule in self.map._rules_by_endpoint.get(endpoint, ()):
+            if rule.suitable_for(values, method):
+                rv = rule.build(values, append_unknown)
+                if rv is not None:
+                    return rv
+
     def build(self, endpoint, values=None, method=None, force_external=False,
               append_unknown=True):
         """Building URLs works pretty much the other way round.  Instead of
                                if you want the builder to ignore those.
         """
         self.map.update()
-        method = method or self.default_method
         if values:
-            values = dict([(k, v) for k, v in values.items() if v is not None])
+            if isinstance(values, MultiDict):
+                values = dict((k, v) for k, v in values.iteritems(multi=True)
+                              if v is not None)
+            else:
+                values = dict((k, v) for k, v in values.iteritems()
+                              if v is not None)
         else:
             values = {}
 
-        for rule in self.map._rules_by_endpoint.get(endpoint, ()):
-            if rule.suitable_for(values, method):
-                rv = rule.build(values, append_unknown)
-                if rv is not None:
-                    break
-        else:
+        rv = self._partial_build(endpoint, values, method, append_unknown)
+        if rv is None:
             raise BuildError(endpoint, values, method)
         subdomain, path = rv
+
         if not force_external and subdomain == self.subdomain:
             return str(urljoin(self.script_name, path.lstrip('/')))
         return str('%s://%s%s%s/%s' % (
     'float':            FloatConverter
 }
 
-from werkzeug.datastructures import ImmutableDict
+from werkzeug.datastructures import ImmutableDict, MultiDict
 Map.default_converters = ImmutableDict(DEFAULT_CONVERTERS)
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.