1# (c) 2005 Ben Bangert
2# This module is part of the Python Paste Project and is released under
3# the MIT License: http://www.opensource.org/licenses/mit-license.php
4"""Registry for handling request-local module globals sanely
5
6Dealing with module globals in a thread-safe way is good if your
7application is the sole responder in a thread, however that approach fails
8to properly account for various scenarios that occur with WSGI applications
9and middleware.
10
11What is actually needed in the case where a module global is desired that
12is always set properly depending on the current request, is a stacked
13thread-local object. Such an object is popped or pushed during the request
14cycle so that it properly represents the object that should be active for
15the current request.
16
17To make it easy to deal with such variables, this module provides a special
18StackedObjectProxy class which you can instantiate and attach to your
19module where you'd like others to access it. The object you'd like this to
20actually "be" during the request is then registered with the
21RegistryManager middleware, which ensures that for the scope of the current
22WSGI application everything will work properly.
23
24Example:
25
26.. code-block:: python
27
28    #yourpackage/__init__.py
29
30    from paste.registry import RegistryManager, StackedObjectProxy
31    myglobal = StackedObjectProxy()
32
33    #wsgi app stack
34    app = RegistryManager(yourapp)
35
36    #inside your wsgi app
37    class yourapp(object):
38        def __call__(self, environ, start_response):
39            obj = someobject  # The request-local object you want to access
40                              # via yourpackage.myglobal
41            if environ.has_key('paste.registry'):
42                environ['paste.registry'].register(myglobal, obj)
43
44You will then be able to import yourpackage anywhere in your WSGI app or in
45the calling stack below it and be assured that it is using the object you
46registered with Registry.
47
48RegistryManager can be in the WSGI stack multiple times, each time it
49appears it registers a new request context.
50
51
52Performance
53===========
54
55The overhead of the proxy object is very minimal, however if you are using
56proxy objects extensively (Thousands of accesses per request or more), there
57are some ways to avoid them. A proxy object runs approximately 3-20x slower
58than direct access to the object, this is rarely your performance bottleneck
59when developing web applications.
60
61Should you be developing a system which may be accessing the proxy object
62thousands of times per request, the performance of the proxy will start to
63become more noticeable. In that circumstance, the problem can be avoided by
64getting at the actual object via the proxy with the ``_current_obj`` function:
65
66.. code-block:: python
67
68    #sessions.py
69    Session = StackedObjectProxy()
70    # ... initialization code, etc.
71
72    # somemodule.py
73    import sessions
74
75    def somefunc():
76        session = sessions.Session._current_obj()
77        # ... tons of session access
78
79This way the proxy is used only once to retrieve the object for the current
80context and the overhead is minimized while still making it easy to access
81the underlying object. The ``_current_obj`` function is preceded by an
82underscore to more likely avoid clashing with the contained object's
83attributes.
84
85**NOTE:** This is *highly* unlikely to be an issue in the vast majority of
86cases, and requires incredibly large amounts of proxy object access before
87one should consider the proxy object to be causing slow-downs. This section
88is provided solely in the extremely rare case that it is an issue so that a
89quick way to work around it is documented.
90
91"""
92import six
93import paste.util.threadinglocal as threadinglocal
94
95__all__ = ['StackedObjectProxy', 'RegistryManager', 'StackedObjectRestorer',
96           'restorer']
97
98class NoDefault(object): pass
99
100class StackedObjectProxy(object):
101    """Track an object instance internally using a stack
102
103    The StackedObjectProxy proxies access to an object internally using a
104    stacked thread-local. This makes it safe for complex WSGI environments
105    where access to the object may be desired in multiple places without
106    having to pass the actual object around.
107
108    New objects are added to the top of the stack with _push_object while
109    objects can be removed with _pop_object.
110
111    """
112    def __init__(self, default=NoDefault, name="Default"):
113        """Create a new StackedObjectProxy
114
115        If a default is given, its used in every thread if no other object
116        has been pushed on.
117
118        """
119        self.__dict__['____name__'] = name
120        self.__dict__['____local__'] = threadinglocal.local()
121        if default is not NoDefault:
122            self.__dict__['____default_object__'] = default
123
124    def __dir__(self):
125        """Return a list of the StackedObjectProxy's and proxied
126        object's (if one exists) names.
127        """
128        dir_list = dir(self.__class__) + self.__dict__.keys()
129        try:
130            dir_list.extend(dir(self._current_obj()))
131        except TypeError:
132            pass
133        dir_list.sort()
134        return dir_list
135
136    def __getattr__(self, attr):
137        return getattr(self._current_obj(), attr)
138
139    def __setattr__(self, attr, value):
140        setattr(self._current_obj(), attr, value)
141
142    def __delattr__(self, name):
143        delattr(self._current_obj(), name)
144
145    def __getitem__(self, key):
146        return self._current_obj()[key]
147
148    def __setitem__(self, key, value):
149        self._current_obj()[key] = value
150
151    def __delitem__(self, key):
152        del self._current_obj()[key]
153
154    def __call__(self, *args, **kw):
155        return self._current_obj()(*args, **kw)
156
157    def __repr__(self):
158        try:
159            return repr(self._current_obj())
160        except (TypeError, AttributeError):
161            return '<%s.%s object at 0x%x>' % (self.__class__.__module__,
162                                               self.__class__.__name__,
163                                               id(self))
164
165    def __iter__(self):
166        return iter(self._current_obj())
167
168    def __len__(self):
169        return len(self._current_obj())
170
171    def __contains__(self, key):
172        return key in self._current_obj()
173
174    def __nonzero__(self):
175        return bool(self._current_obj())
176
177    def _current_obj(self):
178        """Returns the current active object being proxied to
179
180        In the event that no object was pushed, the default object if
181        provided will be used. Otherwise, a TypeError will be raised.
182
183        """
184        try:
185            objects = self.____local__.objects
186        except AttributeError:
187            objects = None
188        if objects:
189            return objects[-1]
190        else:
191            obj = self.__dict__.get('____default_object__', NoDefault)
192            if obj is not NoDefault:
193                return obj
194            else:
195                raise TypeError(
196                    'No object (name: %s) has been registered for this '
197                    'thread' % self.____name__)
198
199    def _push_object(self, obj):
200        """Make ``obj`` the active object for this thread-local.
201
202        This should be used like:
203
204        .. code-block:: python
205
206            obj = yourobject()
207            module.glob = StackedObjectProxy()
208            module.glob._push_object(obj)
209            try:
210                ... do stuff ...
211            finally:
212                module.glob._pop_object(conf)
213
214        """
215        try:
216            self.____local__.objects.append(obj)
217        except AttributeError:
218            self.____local__.objects = []
219            self.____local__.objects.append(obj)
220
221    def _pop_object(self, obj=None):
222        """Remove a thread-local object.
223
224        If ``obj`` is given, it is checked against the popped object and an
225        error is emitted if they don't match.
226
227        """
228        try:
229            popped = self.____local__.objects.pop()
230            if obj and popped is not obj:
231                raise AssertionError(
232                    'The object popped (%s) is not the same as the object '
233                    'expected (%s)' % (popped, obj))
234        except AttributeError:
235            raise AssertionError('No object has been registered for this thread')
236
237    def _object_stack(self):
238        """Returns all of the objects stacked in this container
239
240        (Might return [] if there are none)
241        """
242        try:
243            try:
244                objs = self.____local__.objects
245            except AttributeError:
246                return []
247            return objs[:]
248        except AssertionError:
249            return []
250
251    # The following methods will be swapped for their original versions by
252    # StackedObjectRestorer when restoration is enabled. The original
253    # functions (e.g. _current_obj) will be available at _current_obj_orig
254
255    def _current_obj_restoration(self):
256        request_id = restorer.in_restoration()
257        if request_id:
258            return restorer.get_saved_proxied_obj(self, request_id)
259        return self._current_obj_orig()
260    _current_obj_restoration.__doc__ = \
261        ('%s\n(StackedObjectRestorer restoration enabled)' % \
262         _current_obj.__doc__)
263
264    def _push_object_restoration(self, obj):
265        if not restorer.in_restoration():
266            self._push_object_orig(obj)
267    _push_object_restoration.__doc__ = \
268        ('%s\n(StackedObjectRestorer restoration enabled)' % \
269         _push_object.__doc__)
270
271    def _pop_object_restoration(self, obj=None):
272        if not restorer.in_restoration():
273            self._pop_object_orig(obj)
274    _pop_object_restoration.__doc__ = \
275        ('%s\n(StackedObjectRestorer restoration enabled)' % \
276         _pop_object.__doc__)
277
278class Registry(object):
279    """Track objects and stacked object proxies for removal
280
281    The Registry object is instantiated a single time for the request no
282    matter how many times the RegistryManager is used in a WSGI stack. Each
283    RegistryManager must call ``prepare`` before continuing the call to
284    start a new context for object registering.
285
286    Each context is tracked with a dict inside a list. The last list
287    element is the currently executing context. Each context dict is keyed
288    by the id of the StackedObjectProxy instance being proxied, the value
289    is a tuple of the StackedObjectProxy instance and the object being
290    tracked.
291
292    """
293    def __init__(self):
294        """Create a new Registry object
295
296        ``prepare`` must still be called before this Registry object can be
297        used to register objects.
298
299        """
300        self.reglist = []
301
302    def prepare(self):
303        """Used to create a new registry context
304
305        Anytime a new RegistryManager is called, ``prepare`` needs to be
306        called on the existing Registry object. This sets up a new context
307        for registering objects.
308
309        """
310        self.reglist.append({})
311
312    def register(self, stacked, obj):
313        """Register an object with a StackedObjectProxy"""
314        myreglist = self.reglist[-1]
315        stacked_id = id(stacked)
316        if stacked_id in myreglist:
317            stacked._pop_object(myreglist[stacked_id][1])
318            del myreglist[stacked_id]
319        stacked._push_object(obj)
320        myreglist[stacked_id] = (stacked, obj)
321
322    def multiregister(self, stacklist):
323        """Register a list of tuples
324
325        Similar call semantics as register, except this registers
326        multiple objects at once.
327
328        Example::
329
330            registry.multiregister([(sop, obj), (anothersop, anotherobj)])
331
332        """
333        myreglist = self.reglist[-1]
334        for stacked, obj in stacklist:
335            stacked_id = id(stacked)
336            if stacked_id in myreglist:
337                stacked._pop_object(myreglist[stacked_id][1])
338                del myreglist[stacked_id]
339            stacked._push_object(obj)
340            myreglist[stacked_id] = (stacked, obj)
341
342    # Replace now does the same thing as register
343    replace = register
344
345    def cleanup(self):
346        """Remove all objects from all StackedObjectProxy instances that
347        were tracked at this Registry context"""
348        for stacked, obj in six.itervalues(self.reglist[-1]):
349            stacked._pop_object(obj)
350        self.reglist.pop()
351
352class RegistryManager(object):
353    """Creates and maintains a Registry context
354
355    RegistryManager creates a new registry context for the registration of
356    StackedObjectProxy instances. Multiple RegistryManager's can be in a
357    WSGI stack and will manage the context so that the StackedObjectProxies
358    always proxy to the proper object.
359
360    The object being registered can be any object sub-class, list, or dict.
361
362    Registering objects is done inside a WSGI application under the
363    RegistryManager instance, using the ``environ['paste.registry']``
364    object which is a Registry instance.
365
366    """
367    def __init__(self, application, streaming=False):
368        self.application = application
369        self.streaming = streaming
370
371    def __call__(self, environ, start_response):
372        app_iter = None
373        reg = environ.setdefault('paste.registry', Registry())
374        reg.prepare()
375        if self.streaming:
376            return self.streaming_iter(reg, environ, start_response)
377
378        try:
379            app_iter = self.application(environ, start_response)
380        except Exception as e:
381            # Regardless of if the content is an iterable, generator, list
382            # or tuple, we clean-up right now. If its an iterable/generator
383            # care should be used to ensure the generator has its own ref
384            # to the actual object
385            if environ.get('paste.evalexception'):
386                # EvalException is present in the WSGI stack
387                expected = False
388                for expect in environ.get('paste.expected_exceptions', []):
389                    if isinstance(e, expect):
390                        expected = True
391                if not expected:
392                    # An unexpected exception: save state for EvalException
393                    restorer.save_registry_state(environ)
394            reg.cleanup()
395            raise
396        except:
397            # Save state for EvalException if it's present
398            if environ.get('paste.evalexception'):
399                restorer.save_registry_state(environ)
400            reg.cleanup()
401            raise
402        else:
403            reg.cleanup()
404
405        return app_iter
406
407    def streaming_iter(self, reg, environ, start_response):
408        try:
409            for item in self.application(environ, start_response):
410                yield item
411        except Exception as e:
412            # Regardless of if the content is an iterable, generator, list
413            # or tuple, we clean-up right now. If its an iterable/generator
414            # care should be used to ensure the generator has its own ref
415            # to the actual object
416            if environ.get('paste.evalexception'):
417                # EvalException is present in the WSGI stack
418                expected = False
419                for expect in environ.get('paste.expected_exceptions', []):
420                    if isinstance(e, expect):
421                        expected = True
422                if not expected:
423                    # An unexpected exception: save state for EvalException
424                    restorer.save_registry_state(environ)
425            reg.cleanup()
426            raise
427        except:
428            # Save state for EvalException if it's present
429            if environ.get('paste.evalexception'):
430                restorer.save_registry_state(environ)
431            reg.cleanup()
432            raise
433        else:
434            reg.cleanup()
435
436
437class StackedObjectRestorer(object):
438    """Track StackedObjectProxies and their proxied objects for automatic
439    restoration within EvalException's interactive debugger.
440
441    An instance of this class tracks all StackedObjectProxy state in existence
442    when unexpected exceptions are raised by WSGI applications housed by
443    EvalException and RegistryManager. Like EvalException, this information is
444    stored for the life of the process.
445
446    When an unexpected exception occurs and EvalException is present in the
447    WSGI stack, save_registry_state is intended to be called to store the
448    Registry state and enable automatic restoration on all currently registered
449    StackedObjectProxies.
450
451    With restoration enabled, those StackedObjectProxies' _current_obj
452    (overwritten by _current_obj_restoration) method's strategy is modified:
453    it will return its appropriate proxied object from the restorer when
454    a restoration context is active in the current thread.
455
456    The StackedObjectProxies' _push/pop_object methods strategies are also
457    changed: they no-op when a restoration context is active in the current
458    thread (because the pushing/popping work is all handled by the
459    Registry/restorer).
460
461    The request's Registry objects' reglists are restored from the restorer
462    when a restoration context begins, enabling the Registry methods to work
463    while their changes are tracked by the restorer.
464
465    The overhead of enabling restoration is negligible (another threadlocal
466    access for the changed StackedObjectProxy methods) for normal use outside
467    of a restoration context, but worth mentioning when combined with
468    StackedObjectProxies normal overhead. Once enabled it does not turn off,
469    however:
470
471    o Enabling restoration only occurs after an unexpected exception is
472    detected. The server is likely to be restarted shortly after the exception
473    is raised to fix the cause
474
475    o StackedObjectRestorer is only enabled when EvalException is enabled (not
476    on a production server) and RegistryManager exists in the middleware
477    stack"""
478    def __init__(self):
479        # Registries and their saved reglists by request_id
480        self.saved_registry_states = {}
481        self.restoration_context_id = threadinglocal.local()
482
483    def save_registry_state(self, environ):
484        """Save the state of this request's Registry (if it hasn't already been
485        saved) to the saved_registry_states dict, keyed by the request's unique
486        identifier"""
487        registry = environ.get('paste.registry')
488        if not registry or not len(registry.reglist) or \
489                self.get_request_id(environ) in self.saved_registry_states:
490            # No Registry, no state to save, or this request's state has
491            # already been saved
492            return
493
494        self.saved_registry_states[self.get_request_id(environ)] = \
495            (registry, registry.reglist[:])
496
497        # Tweak the StackedObjectProxies we want to save state for -- change
498        # their methods to act differently when a restoration context is active
499        # in the current thread
500        for reglist in registry.reglist:
501            for stacked, obj in six.itervalues(reglist):
502                self.enable_restoration(stacked)
503
504    def get_saved_proxied_obj(self, stacked, request_id):
505        """Retrieve the saved object proxied by the specified
506        StackedObjectProxy for the request identified by request_id"""
507        # All state for the request identified by request_id
508        reglist = self.saved_registry_states[request_id][1]
509
510        # The top of the stack was current when the exception occurred
511        stack_level = len(reglist) - 1
512        stacked_id = id(stacked)
513        while True:
514            if stack_level < 0:
515                # Nothing registered: Call _current_obj_orig to raise a
516                # TypeError
517                return stacked._current_obj_orig()
518            context = reglist[stack_level]
519            if stacked_id in context:
520                break
521            # This StackedObjectProxy may not have been registered by the
522            # RegistryManager that was active when the exception was raised --
523            # continue searching down the stack until it's found
524            stack_level -= 1
525        return context[stacked_id][1]
526
527    def enable_restoration(self, stacked):
528        """Replace the specified StackedObjectProxy's methods with their
529        respective restoration versions.
530
531        _current_obj_restoration forces recovery of the saved proxied object
532        when a restoration context is active in the current thread.
533
534        _push/pop_object_restoration avoid pushing/popping data
535        (pushing/popping is only done at the Registry level) when a restoration
536        context is active in the current thread"""
537        if '_current_obj_orig' in stacked.__dict__:
538            # Restoration already enabled
539            return
540
541        for func_name in ('_current_obj', '_push_object', '_pop_object'):
542            orig_func = getattr(stacked, func_name)
543            restoration_func = getattr(stacked, func_name + '_restoration')
544            stacked.__dict__[func_name + '_orig'] = orig_func
545            stacked.__dict__[func_name] = restoration_func
546
547    def get_request_id(self, environ):
548        """Return a unique identifier for the current request"""
549        from paste.evalexception.middleware import get_debug_count
550        return get_debug_count(environ)
551
552    def restoration_begin(self, request_id):
553        """Enable a restoration context in the current thread for the specified
554        request_id"""
555        if request_id in self.saved_registry_states:
556            # Restore the old Registry object's state
557            registry, reglist = self.saved_registry_states[request_id]
558            registry.reglist = reglist
559
560        self.restoration_context_id.request_id = request_id
561
562    def restoration_end(self):
563        """Register a restoration context as finished, if one exists"""
564        try:
565            del self.restoration_context_id.request_id
566        except AttributeError:
567            pass
568
569    def in_restoration(self):
570        """Determine if a restoration context is active for the current thread.
571        Returns the request_id it's active for if so, otherwise False"""
572        return getattr(self.restoration_context_id, 'request_id', False)
573
574restorer = StackedObjectRestorer()
575
576
577# Paste Deploy entry point
578def make_registry_manager(app, global_conf):
579    return RegistryManager(app)
580
581make_registry_manager.__doc__ = RegistryManager.__doc__
582