Latest Version: 0.9.6.2
/Users/bbangert/Programming/Python/Paste/paste/registry.py
0001# (c) 2005 Ben Bangert
0002# This module is part of the Python Paste Project and is released under
0003# the MIT License: http://www.opensource.org/licenses/mit-license.php
0004"""Registry for handling request-local module globals sanely
0005
0006Dealing with module globals in a thread-safe way is good if your
0007application is the sole responder in a thread, however that approach fails
0008to properly account for various scenarios that occur with WSGI applications
0009and middleware.
0010
0011What is actually needed in the case where a module global is desired that
0012is always set properly depending on the current request, is a stacked
0013thread-local object. Such an object is popped or pushed during the request
0014cycle so that it properly represents the object that should be active for
0015the current request.
0016
0017To make it easy to deal with such variables, this module provides a special
0018StackedObjectProxy class which you can instantiate and attach to your
0019module where you'd like others to access it. The object you'd like this to
0020actually "be" during the request is then registered with the
0021RegistryManager middleware, which ensures that for the scope of the current
0022WSGI application everything will work properly.
0023
0024Example:
0025
0026.. code-block:: Python
0027    
0028    #yourpackage/__init__.py
0029    
0030    from paste.registry import RegistryManager, StackedObjectProxy
0031    myglobal = StackedObjectProxy()
0032    
0033    #wsgi app stack
0034    app = RegistryManager(yourapp)
0035    
0036    #inside your wsgi app
0037    class yourapp(object):
0038        def __call__(self, environ, start_response):
0039            obj = someobject  # The request-local object you want to access
0040                              # via yourpackage.myglobal
0041            if environ.has_key('paste.registry'):
0042                environ['paste.registry'].register(myglobal, obj)
0043
0044You will then be able to import yourpackage anywhere in your WSGI app or in
0045the calling stack below it and be assured that it is using the object you
0046registered with Registry.
0047
0048RegistryManager can be in the WSGI stack multiple times, each time it
0049appears it registers a new request context.
0050
0051
0052Performance
0053===========
0054
0055The overhead of the proxy object is very minimal, however if you are using
0056proxy objects extensively (Thousands of accesses per request or more), there
0057are some ways to avoid them. A proxy object runs approximately 3-20x slower
0058than direct access to the object, this is rarely your performance bottleneck
0059when developing web applications.
0060
0061Should you be developing a system which may be accessing the proxy object
0062thousands of times per request, the performance of the proxy will start to
0063become more noticeable. In that circumstance, the problem can be avoided by
0064getting at the actual object via the proxy with the ``_current_obj`` function:
0065
0066.. code-block:: Python
0067    
0068    #sessions.py
0069    Session = StackedObjectProxy()
0070    # ... initialization code, etc.
0071    
0072    # somemodule.py
0073    import sessions
0074    
0075    def somefunc():
0076        session = sessions.Session._current_obj()
0077        # ... tons of session access
0078
0079This way the proxy is used only once to retrieve the object for the current
0080context and the overhead is minimized while still making it easy to access
0081the underlying object. The ``_current_obj`` function is preceded by an
0082underscore to more likely avoid clashing with the contained object's
0083attributes.
0084
0085**NOTE:** This is *highly* unlikely to be an issue in the vast majority of
0086cases, and requires incredibly large amounts of proxy object access before
0087one should consider the proxy object to be causing slow-downs. This section
0088is provided solely in the extremely rare case that it is an issue so that a
0089quick way to work around it is documented.
0090
0091"""
0092import sys
0093import paste.util.threadinglocal as threadinglocal
0094
0095__all__ = ['StackedObjectProxy', 'RegistryManager', 'StackedObjectRestorer',
0096           'restorer']
0097
0098class NoDefault(object): pass
0099
0100class StackedObjectProxy(object):
0101    """Track an object instance internally using a stack
0102    
0103    The StackedObjectProxy proxies access to an object internally using a
0104    stacked thread-local. This makes it safe for complex WSGI environments
0105    where access to the object may be desired in multiple places without
0106    having to pass the actual object around.
0107    
0108    New objects are added to the top of the stack with _push_object while
0109    objects can be removed with _pop_object. 
0110    
0111    """
0112    def __init__(self, default=NoDefault, name="Default"):
0113        """Create a new StackedObjectProxy
0114        
0115        If a default is given, its used in every thread if no other object
0116        has been pushed on.
0117        
0118        """
0119        self.__dict__['____name__'] = name
0120        self.__dict__['____local__'] = threadinglocal.local()
0121        if default is not NoDefault:
0122            self.__dict__['____default_object__'] = default
0123
0124    def __getattr__(self, attr):
0125        return getattr(self._current_obj(), attr)
0126
0127    def __setattr__(self, attr, value):
0128        setattr(self._current_obj(), attr, value)
0129
0130    def __delattr__(self, name):
0131        delattr(self._current_obj(), name)
0132
0133    def __getitem__(self, key):
0134        return self._current_obj()[key]
0135
0136    def __setitem__(self, key, value):
0137        self._current_obj()[key] = value
0138
0139    def __delitem__(self, key):
0140        del self._current_obj()[key]
0141
0142    def __call__(self, *args, **kw):
0143        return self._current_obj()(*args, **kw)
0144
0145    def __repr__(self):
0146        try:
0147            return repr(self._current_obj())
0148        except (TypeError, AttributeError):
0149            return '<%s.%s object at 0x%x>' % (self.__class__.__module__,
0150                                               self.__class__.__name__,
0151                                               id(self))
0152
0153    def __iter__(self):
0154        return iter(self._current_obj())
0155
0156    def __len__(self):
0157        return len(self._current_obj())
0158
0159    def __contains__(self, key):
0160        return key in self._current_obj()
0161
0162    def __nonzero__(self):
0163        return bool(self._current_obj())
0164
0165    def _current_obj(self):
0166        """Returns the current active object being proxied to
0167        
0168        In the event that no object was pushed, the default object if
0169        provided will be used. Otherwise, a TypeError will be raised.
0170        
0171        """
0172        objects = getattr(self.____local__, 'objects', None)
0173        if objects:
0174            return objects[-1]
0175        else:
0176            obj = self.__dict__.get('____default_object__', NoDefault)
0177            if obj is not NoDefault:
0178                return obj
0179            else:
0180                raise TypeError(
0181                    'No object (name: %s) has been registered for this '
0182                    'thread' % self.____name__)
0183
0184    def _push_object(self, obj):
0185        """Make ``obj`` the active object for this thread-local.
0186        
0187        This should be used like:
0188        
0189        .. code-block:: Python
0190
0191            obj = yourobject()
0192            module.glob = StackedObjectProxy()
0193            module.glob._push_object(obj)
0194            try:
0195                ... do stuff ...
0196            finally:
0197                module.glob._pop_object(conf)
0198        
0199        """
0200        if not hasattr(self.____local__, 'objects'):
0201            self.____local__.objects = []
0202        self.____local__.objects.append(obj)
0203
0204    def _pop_object(self, obj=None):
0205        """Remove a thread-local object.
0206        
0207        If ``obj`` is given, it is checked against the popped object and an
0208        error is emitted if they don't match.
0209        
0210        """
0211        if not hasattr(self.____local__, 'objects'):
0212            raise AssertionError('No object has been registered for this thread')
0213        popped = self.____local__.objects.pop()
0214        if obj:
0215            if popped is not obj:
0216                raise AssertionError(
0217                    'The object popped (%s) is not the same as the object '
0218                    'expected (%s)' % (popped, obj))
0219
0220    def _object_stack(self):
0221        """Returns all of the objects stacked in this container
0222
0223        (Might return [] if there are none)
0224        """
0225        try:
0226            return self.____local__.objects[:]
0227        except AssertionError:
0228            return []
0229
0230    # The following methods will be swapped for their original versions by
0231    # StackedObjectRestorer when restoration is enabled. The original
0232    # functions (e.g. _current_obj) will be available at _current_obj_orig
0233
0234    def _current_obj_restoration(self):
0235        request_id = restorer.in_restoration()
0236        if request_id:
0237            return restorer.get_saved_proxied_obj(self, request_id)
0238        return self._current_obj_orig()
0239    _current_obj_restoration.__doc__ =           ('%s\n(StackedObjectRestorer restoration enabled)' %            _current_obj.__doc__)
0242
0243    def _push_object_restoration(self, obj):
0244        if not restorer.in_restoration():
0245            self._push_object_orig(obj)
0246    _push_object_restoration.__doc__ =           ('%s\n(StackedObjectRestorer restoration enabled)' %            _push_object.__doc__)
0249
0250    def _pop_object_restoration(self, obj=None):
0251        if not restorer.in_restoration():
0252            self._pop_object_orig(obj)
0253    _pop_object_restoration.__doc__ =           ('%s\n(StackedObjectRestorer restoration enabled)' %            _pop_object.__doc__)
0256
0257class Registry(object):
0258    """Track objects and stacked object proxies for removal
0259    
0260    The Registry object is instantiated a single time for the request no
0261    matter how many times the RegistryManager is used in a WSGI stack. Each
0262    RegistryManager must call ``prepare`` before continuing the call to
0263    start a new context for object registering.
0264    
0265    Each context is tracked with a dict inside a list. The last list
0266    element is the currently executing context. Each context dict is keyed
0267    by the id of the StackedObjectProxy instance being proxied, the value
0268    is a tuple of the StackedObjectProxy instance and the object being
0269    tracked.
0270    
0271    """
0272    def __init__(self):
0273        """Create a new Registry object
0274        
0275        ``prepare`` must still be called before this Registry object can be
0276        used to register objects.
0277        
0278        """
0279        self.reglist = []
0280
0281    def prepare(self):
0282        """Used to create a new registry context
0283        
0284        Anytime a new RegistryManager is called, ``prepare`` needs to be
0285        called on the existing Registry object. This sets up a new context
0286        for registering objects.
0287        
0288        """
0289        self.reglist.append({})
0290
0291    def register(self, stacked, obj):
0292        """Register an object with a StackedObjectProxy"""
0293        stacked._push_object(obj)
0294        myreglist = self.reglist[-1]
0295        myreglist[id(stacked)] = (stacked, obj)
0296
0297    def replace(self, stacked, obj):
0298        """Replace the object referenced by a StackedObjectProxy with a
0299        different object
0300
0301        In the event that no object has been registered, the new object will
0302        be registered.
0303        """
0304        myreglist = self.reglist[-1]
0305        if id(stacked) in myreglist:
0306            stacked._pop_object(myreglist[id(stacked)][1])
0307        self.register(stacked, obj)
0308
0309    def cleanup(self):
0310        """Remove all objects from all StackedObjectProxy instances that
0311        were tracked at this Registry context"""
0312        for id, val in self.reglist[-1].iteritems():
0313            stacked, obj = val
0314            stacked._pop_object(obj)
0315        self.reglist.pop()
0316
0317class RegistryManager(object):
0318    """Creates and maintains a Registry context
0319    
0320    RegistryManager creates a new registry context for the registration of
0321    StackedObjectProxy instances. Multiple RegistryManager's can be in a
0322    WSGI stack and will manage the context so that the StackedObjectProxies
0323    always proxy to the proper object.
0324    
0325    The object being registered can be any object sub-class, list, or dict.
0326    
0327    Registering objects is done inside a WSGI application under the
0328    RegistryManager instance, using the ``environ['paste.registry']``
0329    object which is a Registry instance.
0330        
0331    """
0332    def __init__(self, application):
0333        self.application = application
0334
0335    def __call__(self, environ, start_response):
0336        app_iter = None
0337        reg = environ.setdefault('paste.registry', Registry())
0338        reg.prepare()
0339        try:
0340            app_iter = self.application(environ, start_response)
0341        except Exception, e:
0342            # Regardless of if the content is an iterable, generator, list
0343            # or tuple, we clean-up right now. If its an iterable/generator
0344            # care should be used to ensure the generator has its own ref
0345            # to the actual object
0346            if environ.get('paste.evalexception'):
0347                # EvalException is present in the WSGI stack
0348                expected = False
0349                for expect in environ.get('paste.expected_exceptions', []):
0350                    if isinstance(e, expect):
0351                        expected = True
0352                if not expected:
0353                    # An unexpected exception: save state for EvalException
0354                    restorer.save_registry_state(environ)
0355            reg.cleanup()
0356            raise
0357        except:
0358            # Save state for EvalException if it's present
0359            if environ.get('paste.evalexception'):
0360                restorer.save_registry_state(environ)
0361            reg.cleanup()
0362            raise
0363        else:
0364            reg.cleanup()
0365
0366        return app_iter
0367
0368class StackedObjectRestorer(object):
0369    """Track StackedObjectProxies and their proxied objects for automatic
0370    restoration within EvalException's interactive debugger.
0371
0372    An instance of this class tracks all StackedObjectProxy state in existence
0373    when unexpected exceptions are raised by WSGI applications housed by
0374    EvalException and RegistryManager. Like EvalException, this information is
0375    stored for the life of the process.
0376
0377    When an unexpected exception occurs and EvalException is present in the
0378    WSGI stack, save_registry_state is intended to be called to store the
0379    Registry state and enable automatic restoration on all currently registered
0380    StackedObjectProxies.
0381
0382    With restoration enabled, those StackedObjectProxies' _current_obj
0383    (overwritten by _current_obj_restoration) method's strategy is modified:
0384    it will return its appropriate proxied object from the restorer when
0385    a restoration context is active in the current thread.
0386
0387    The StackedObjectProxies' _push/pop_object methods strategies are also
0388    changed: they no-op when a restoration context is active in the current
0389    thread (because the pushing/popping work is all handled by the
0390    Registry/restorer).
0391
0392    The request's Registry objects' reglists are restored from the restorer
0393    when a restoration context begins, enabling the Registry methods to work
0394    while their changes are tracked by the restorer.
0395
0396    The overhead of enabling restoration is negligible (another threadlocal
0397    access for the changed StackedObjectProxy methods) for normal use outside
0398    of a restoration context, but worth mentioning when combined with
0399    StackedObjectProxies normal overhead. Once enabled it does not turn off,
0400    however:
0401
0402    o Enabling restoration only occurs after an unexpected exception is
0403    detected. The server is likely to be restarted shortly after the exception
0404    is raised to fix the cause
0405
0406    o StackedObjectRestorer is only enabled when EvalException is enabled (not
0407    on a production server) and RegistryManager exists in the middleware
0408    stack"""
0409    def __init__(self):
0410        # Registries and their saved reglists by request_id
0411        self.saved_registry_states = {}
0412        self.restoration_context_id = threadinglocal.local()
0413
0414    def save_registry_state(self, environ):
0415        """Save the state of this request's Registry (if it hasn't already been
0416        saved) to the saved_registry_states dict, keyed by the request's unique
0417        identifier"""
0418        registry = environ.get('paste.registry')
0419        if not registry or not len(registry.reglist) or                   self.get_request_id(environ) in self.saved_registry_states:
0421            # No Registry, no state to save, or this request's state has
0422            # already been saved
0423            return
0424
0425        self.saved_registry_states[self.get_request_id(environ)] =               (registry, registry.reglist[:])
0427
0428        # Tweak the StackedObjectProxies we want to save state for -- change
0429        # their methods to act differently when a restoration context is active
0430        # in the current thread
0431        for reglist in registry.reglist:
0432            for stacked, obj in reglist.itervalues():
0433                self.enable_restoration(stacked)
0434
0435    def get_saved_proxied_obj(self, stacked, request_id):
0436        """Retrieve the saved object proxied by the specified
0437        StackedObjectProxy for the request identified by request_id"""
0438        # All state for the request identified by request_id
0439        reglist = self.saved_registry_states[request_id][1]
0440
0441        # The top of the stack was current when the exception occurred
0442        stack_level = len(reglist) - 1
0443        stacked_id = id(stacked)
0444        while True:
0445            if stack_level < 0:
0446                # Nothing registered: Call _current_obj_orig to raise a
0447                # TypeError
0448                return stacked._current_obj_orig()
0449            context = reglist[stack_level]
0450            if stacked_id in context:
0451                break
0452            # This StackedObjectProxy may not have been registered by the
0453            # RegistryManager that was active when the exception was raised --
0454            # continue searching down the stack until it's found
0455            stack_level -= 1
0456        return context[stacked_id][1]
0457
0458    def enable_restoration(self, stacked):
0459        """Replace the specified StackedObjectProxy's methods with their
0460        respective restoration versions.
0461
0462        _current_obj_restoration forces recovery of the saved proxied object
0463        when a restoration context is active in the current thread.
0464
0465        _push/pop_object_restoration avoid pushing/popping data
0466        (pushing/popping is only done at the Registry level) when a restoration
0467        context is active in the current thread"""
0468        if '_current_obj_orig' in stacked.__dict__:
0469            # Restoration already enabled
0470            return
0471
0472        for func_name in ('_current_obj', '_push_object', '_pop_object'):
0473            orig_func = getattr(stacked, func_name)
0474            restoration_func = getattr(stacked, func_name + '_restoration')
0475            stacked.__dict__[func_name + '_orig'] = orig_func
0476            stacked.__dict__[func_name] = restoration_func
0477
0478    def get_request_id(self, environ):
0479        """Return a unique identifier for the current request"""
0480        from paste.evalexception.middleware import get_debug_count
0481        return get_debug_count(environ)
0482
0483    def restoration_begin(self, request_id):
0484        """Enable a restoration context in the current thread for the specified
0485        request_id"""
0486        if request_id in self.saved_registry_states:
0487            # Restore the old Registry object's state
0488            registry, reglist = self.saved_registry_states[request_id]
0489            registry.reglist = reglist
0490
0491        self.restoration_context_id.request_id = request_id
0492
0493    def restoration_end(self):
0494        """Register a restoration context as finished, if one exists"""
0495        try:
0496            del self.restoration_context_id.request_id
0497        except AttributeError:
0498            pass
0499
0500    def in_restoration(self):
0501        """Determine if a restoration context is active for the current thread.
0502        Returns the request_id it's active for if so, otherwise False"""
0503        return getattr(self.restoration_context_id, 'request_id', False)
0504
0505restorer = StackedObjectRestorer()
0506
0507
0508# Paste Deploy entry point
0509def make_registry_manager(app, global_conf):
0510    return RegistryManager(app)
0511
0512make_registry_manager.__doc__ = RegistryManager.__doc__

Top