From 42b22881290e00e06b840dee1e42f0f5ef044d47 Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Mon, 7 Mar 2016 14:05:52 -0800 Subject: tox.ini: Add py35 to envlist --- paste/registry.py | 581 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 581 insertions(+) create mode 100644 paste/registry.py (limited to 'paste/registry.py') diff --git a/paste/registry.py b/paste/registry.py new file mode 100644 index 0000000..908bc0d --- /dev/null +++ b/paste/registry.py @@ -0,0 +1,581 @@ +# (c) 2005 Ben Bangert +# This module is part of the Python Paste Project and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php +"""Registry for handling request-local module globals sanely + +Dealing with module globals in a thread-safe way is good if your +application is the sole responder in a thread, however that approach fails +to properly account for various scenarios that occur with WSGI applications +and middleware. + +What is actually needed in the case where a module global is desired that +is always set properly depending on the current request, is a stacked +thread-local object. Such an object is popped or pushed during the request +cycle so that it properly represents the object that should be active for +the current request. + +To make it easy to deal with such variables, this module provides a special +StackedObjectProxy class which you can instantiate and attach to your +module where you'd like others to access it. The object you'd like this to +actually "be" during the request is then registered with the +RegistryManager middleware, which ensures that for the scope of the current +WSGI application everything will work properly. + +Example: + +.. code-block:: python + + #yourpackage/__init__.py + + from paste.registry import RegistryManager, StackedObjectProxy + myglobal = StackedObjectProxy() + + #wsgi app stack + app = RegistryManager(yourapp) + + #inside your wsgi app + class yourapp(object): + def __call__(self, environ, start_response): + obj = someobject # The request-local object you want to access + # via yourpackage.myglobal + if environ.has_key('paste.registry'): + environ['paste.registry'].register(myglobal, obj) + +You will then be able to import yourpackage anywhere in your WSGI app or in +the calling stack below it and be assured that it is using the object you +registered with Registry. + +RegistryManager can be in the WSGI stack multiple times, each time it +appears it registers a new request context. + + +Performance +=========== + +The overhead of the proxy object is very minimal, however if you are using +proxy objects extensively (Thousands of accesses per request or more), there +are some ways to avoid them. A proxy object runs approximately 3-20x slower +than direct access to the object, this is rarely your performance bottleneck +when developing web applications. + +Should you be developing a system which may be accessing the proxy object +thousands of times per request, the performance of the proxy will start to +become more noticeable. In that circumstance, the problem can be avoided by +getting at the actual object via the proxy with the ``_current_obj`` function: + +.. code-block:: python + + #sessions.py + Session = StackedObjectProxy() + # ... initialization code, etc. + + # somemodule.py + import sessions + + def somefunc(): + session = sessions.Session._current_obj() + # ... tons of session access + +This way the proxy is used only once to retrieve the object for the current +context and the overhead is minimized while still making it easy to access +the underlying object. The ``_current_obj`` function is preceded by an +underscore to more likely avoid clashing with the contained object's +attributes. + +**NOTE:** This is *highly* unlikely to be an issue in the vast majority of +cases, and requires incredibly large amounts of proxy object access before +one should consider the proxy object to be causing slow-downs. This section +is provided solely in the extremely rare case that it is an issue so that a +quick way to work around it is documented. + +""" +import six +import paste.util.threadinglocal as threadinglocal + +__all__ = ['StackedObjectProxy', 'RegistryManager', 'StackedObjectRestorer', + 'restorer'] + +class NoDefault(object): pass + +class StackedObjectProxy(object): + """Track an object instance internally using a stack + + The StackedObjectProxy proxies access to an object internally using a + stacked thread-local. This makes it safe for complex WSGI environments + where access to the object may be desired in multiple places without + having to pass the actual object around. + + New objects are added to the top of the stack with _push_object while + objects can be removed with _pop_object. + + """ + def __init__(self, default=NoDefault, name="Default"): + """Create a new StackedObjectProxy + + If a default is given, its used in every thread if no other object + has been pushed on. + + """ + self.__dict__['____name__'] = name + self.__dict__['____local__'] = threadinglocal.local() + if default is not NoDefault: + self.__dict__['____default_object__'] = default + + def __dir__(self): + """Return a list of the StackedObjectProxy's and proxied + object's (if one exists) names. + """ + dir_list = dir(self.__class__) + self.__dict__.keys() + try: + dir_list.extend(dir(self._current_obj())) + except TypeError: + pass + dir_list.sort() + return dir_list + + def __getattr__(self, attr): + return getattr(self._current_obj(), attr) + + def __setattr__(self, attr, value): + setattr(self._current_obj(), attr, value) + + def __delattr__(self, name): + delattr(self._current_obj(), name) + + def __getitem__(self, key): + return self._current_obj()[key] + + def __setitem__(self, key, value): + self._current_obj()[key] = value + + def __delitem__(self, key): + del self._current_obj()[key] + + def __call__(self, *args, **kw): + return self._current_obj()(*args, **kw) + + def __repr__(self): + try: + return repr(self._current_obj()) + except (TypeError, AttributeError): + return '<%s.%s object at 0x%x>' % (self.__class__.__module__, + self.__class__.__name__, + id(self)) + + def __iter__(self): + return iter(self._current_obj()) + + def __len__(self): + return len(self._current_obj()) + + def __contains__(self, key): + return key in self._current_obj() + + def __nonzero__(self): + return bool(self._current_obj()) + + def _current_obj(self): + """Returns the current active object being proxied to + + In the event that no object was pushed, the default object if + provided will be used. Otherwise, a TypeError will be raised. + + """ + try: + objects = self.____local__.objects + except AttributeError: + objects = None + if objects: + return objects[-1] + else: + obj = self.__dict__.get('____default_object__', NoDefault) + if obj is not NoDefault: + return obj + else: + raise TypeError( + 'No object (name: %s) has been registered for this ' + 'thread' % self.____name__) + + def _push_object(self, obj): + """Make ``obj`` the active object for this thread-local. + + This should be used like: + + .. code-block:: python + + obj = yourobject() + module.glob = StackedObjectProxy() + module.glob._push_object(obj) + try: + ... do stuff ... + finally: + module.glob._pop_object(conf) + + """ + try: + self.____local__.objects.append(obj) + except AttributeError: + self.____local__.objects = [] + self.____local__.objects.append(obj) + + def _pop_object(self, obj=None): + """Remove a thread-local object. + + If ``obj`` is given, it is checked against the popped object and an + error is emitted if they don't match. + + """ + try: + popped = self.____local__.objects.pop() + if obj and popped is not obj: + raise AssertionError( + 'The object popped (%s) is not the same as the object ' + 'expected (%s)' % (popped, obj)) + except AttributeError: + raise AssertionError('No object has been registered for this thread') + + def _object_stack(self): + """Returns all of the objects stacked in this container + + (Might return [] if there are none) + """ + try: + try: + objs = self.____local__.objects + except AttributeError: + return [] + return objs[:] + except AssertionError: + return [] + + # The following methods will be swapped for their original versions by + # StackedObjectRestorer when restoration is enabled. The original + # functions (e.g. _current_obj) will be available at _current_obj_orig + + def _current_obj_restoration(self): + request_id = restorer.in_restoration() + if request_id: + return restorer.get_saved_proxied_obj(self, request_id) + return self._current_obj_orig() + _current_obj_restoration.__doc__ = \ + ('%s\n(StackedObjectRestorer restoration enabled)' % \ + _current_obj.__doc__) + + def _push_object_restoration(self, obj): + if not restorer.in_restoration(): + self._push_object_orig(obj) + _push_object_restoration.__doc__ = \ + ('%s\n(StackedObjectRestorer restoration enabled)' % \ + _push_object.__doc__) + + def _pop_object_restoration(self, obj=None): + if not restorer.in_restoration(): + self._pop_object_orig(obj) + _pop_object_restoration.__doc__ = \ + ('%s\n(StackedObjectRestorer restoration enabled)' % \ + _pop_object.__doc__) + +class Registry(object): + """Track objects and stacked object proxies for removal + + The Registry object is instantiated a single time for the request no + matter how many times the RegistryManager is used in a WSGI stack. Each + RegistryManager must call ``prepare`` before continuing the call to + start a new context for object registering. + + Each context is tracked with a dict inside a list. The last list + element is the currently executing context. Each context dict is keyed + by the id of the StackedObjectProxy instance being proxied, the value + is a tuple of the StackedObjectProxy instance and the object being + tracked. + + """ + def __init__(self): + """Create a new Registry object + + ``prepare`` must still be called before this Registry object can be + used to register objects. + + """ + self.reglist = [] + + def prepare(self): + """Used to create a new registry context + + Anytime a new RegistryManager is called, ``prepare`` needs to be + called on the existing Registry object. This sets up a new context + for registering objects. + + """ + self.reglist.append({}) + + def register(self, stacked, obj): + """Register an object with a StackedObjectProxy""" + myreglist = self.reglist[-1] + stacked_id = id(stacked) + if stacked_id in myreglist: + stacked._pop_object(myreglist[stacked_id][1]) + del myreglist[stacked_id] + stacked._push_object(obj) + myreglist[stacked_id] = (stacked, obj) + + def multiregister(self, stacklist): + """Register a list of tuples + + Similar call semantics as register, except this registers + multiple objects at once. + + Example:: + + registry.multiregister([(sop, obj), (anothersop, anotherobj)]) + + """ + myreglist = self.reglist[-1] + for stacked, obj in stacklist: + stacked_id = id(stacked) + if stacked_id in myreglist: + stacked._pop_object(myreglist[stacked_id][1]) + del myreglist[stacked_id] + stacked._push_object(obj) + myreglist[stacked_id] = (stacked, obj) + + # Replace now does the same thing as register + replace = register + + def cleanup(self): + """Remove all objects from all StackedObjectProxy instances that + were tracked at this Registry context""" + for stacked, obj in six.itervalues(self.reglist[-1]): + stacked._pop_object(obj) + self.reglist.pop() + +class RegistryManager(object): + """Creates and maintains a Registry context + + RegistryManager creates a new registry context for the registration of + StackedObjectProxy instances. Multiple RegistryManager's can be in a + WSGI stack and will manage the context so that the StackedObjectProxies + always proxy to the proper object. + + The object being registered can be any object sub-class, list, or dict. + + Registering objects is done inside a WSGI application under the + RegistryManager instance, using the ``environ['paste.registry']`` + object which is a Registry instance. + + """ + def __init__(self, application, streaming=False): + self.application = application + self.streaming = streaming + + def __call__(self, environ, start_response): + app_iter = None + reg = environ.setdefault('paste.registry', Registry()) + reg.prepare() + if self.streaming: + return self.streaming_iter(reg, environ, start_response) + + try: + app_iter = self.application(environ, start_response) + except Exception as e: + # Regardless of if the content is an iterable, generator, list + # or tuple, we clean-up right now. If its an iterable/generator + # care should be used to ensure the generator has its own ref + # to the actual object + if environ.get('paste.evalexception'): + # EvalException is present in the WSGI stack + expected = False + for expect in environ.get('paste.expected_exceptions', []): + if isinstance(e, expect): + expected = True + if not expected: + # An unexpected exception: save state for EvalException + restorer.save_registry_state(environ) + reg.cleanup() + raise + except: + # Save state for EvalException if it's present + if environ.get('paste.evalexception'): + restorer.save_registry_state(environ) + reg.cleanup() + raise + else: + reg.cleanup() + + return app_iter + + def streaming_iter(self, reg, environ, start_response): + try: + for item in self.application(environ, start_response): + yield item + except Exception as e: + # Regardless of if the content is an iterable, generator, list + # or tuple, we clean-up right now. If its an iterable/generator + # care should be used to ensure the generator has its own ref + # to the actual object + if environ.get('paste.evalexception'): + # EvalException is present in the WSGI stack + expected = False + for expect in environ.get('paste.expected_exceptions', []): + if isinstance(e, expect): + expected = True + if not expected: + # An unexpected exception: save state for EvalException + restorer.save_registry_state(environ) + reg.cleanup() + raise + except: + # Save state for EvalException if it's present + if environ.get('paste.evalexception'): + restorer.save_registry_state(environ) + reg.cleanup() + raise + else: + reg.cleanup() + + +class StackedObjectRestorer(object): + """Track StackedObjectProxies and their proxied objects for automatic + restoration within EvalException's interactive debugger. + + An instance of this class tracks all StackedObjectProxy state in existence + when unexpected exceptions are raised by WSGI applications housed by + EvalException and RegistryManager. Like EvalException, this information is + stored for the life of the process. + + When an unexpected exception occurs and EvalException is present in the + WSGI stack, save_registry_state is intended to be called to store the + Registry state and enable automatic restoration on all currently registered + StackedObjectProxies. + + With restoration enabled, those StackedObjectProxies' _current_obj + (overwritten by _current_obj_restoration) method's strategy is modified: + it will return its appropriate proxied object from the restorer when + a restoration context is active in the current thread. + + The StackedObjectProxies' _push/pop_object methods strategies are also + changed: they no-op when a restoration context is active in the current + thread (because the pushing/popping work is all handled by the + Registry/restorer). + + The request's Registry objects' reglists are restored from the restorer + when a restoration context begins, enabling the Registry methods to work + while their changes are tracked by the restorer. + + The overhead of enabling restoration is negligible (another threadlocal + access for the changed StackedObjectProxy methods) for normal use outside + of a restoration context, but worth mentioning when combined with + StackedObjectProxies normal overhead. Once enabled it does not turn off, + however: + + o Enabling restoration only occurs after an unexpected exception is + detected. The server is likely to be restarted shortly after the exception + is raised to fix the cause + + o StackedObjectRestorer is only enabled when EvalException is enabled (not + on a production server) and RegistryManager exists in the middleware + stack""" + def __init__(self): + # Registries and their saved reglists by request_id + self.saved_registry_states = {} + self.restoration_context_id = threadinglocal.local() + + def save_registry_state(self, environ): + """Save the state of this request's Registry (if it hasn't already been + saved) to the saved_registry_states dict, keyed by the request's unique + identifier""" + registry = environ.get('paste.registry') + if not registry or not len(registry.reglist) or \ + self.get_request_id(environ) in self.saved_registry_states: + # No Registry, no state to save, or this request's state has + # already been saved + return + + self.saved_registry_states[self.get_request_id(environ)] = \ + (registry, registry.reglist[:]) + + # Tweak the StackedObjectProxies we want to save state for -- change + # their methods to act differently when a restoration context is active + # in the current thread + for reglist in registry.reglist: + for stacked, obj in six.itervalues(reglist): + self.enable_restoration(stacked) + + def get_saved_proxied_obj(self, stacked, request_id): + """Retrieve the saved object proxied by the specified + StackedObjectProxy for the request identified by request_id""" + # All state for the request identified by request_id + reglist = self.saved_registry_states[request_id][1] + + # The top of the stack was current when the exception occurred + stack_level = len(reglist) - 1 + stacked_id = id(stacked) + while True: + if stack_level < 0: + # Nothing registered: Call _current_obj_orig to raise a + # TypeError + return stacked._current_obj_orig() + context = reglist[stack_level] + if stacked_id in context: + break + # This StackedObjectProxy may not have been registered by the + # RegistryManager that was active when the exception was raised -- + # continue searching down the stack until it's found + stack_level -= 1 + return context[stacked_id][1] + + def enable_restoration(self, stacked): + """Replace the specified StackedObjectProxy's methods with their + respective restoration versions. + + _current_obj_restoration forces recovery of the saved proxied object + when a restoration context is active in the current thread. + + _push/pop_object_restoration avoid pushing/popping data + (pushing/popping is only done at the Registry level) when a restoration + context is active in the current thread""" + if '_current_obj_orig' in stacked.__dict__: + # Restoration already enabled + return + + for func_name in ('_current_obj', '_push_object', '_pop_object'): + orig_func = getattr(stacked, func_name) + restoration_func = getattr(stacked, func_name + '_restoration') + stacked.__dict__[func_name + '_orig'] = orig_func + stacked.__dict__[func_name] = restoration_func + + def get_request_id(self, environ): + """Return a unique identifier for the current request""" + from paste.evalexception.middleware import get_debug_count + return get_debug_count(environ) + + def restoration_begin(self, request_id): + """Enable a restoration context in the current thread for the specified + request_id""" + if request_id in self.saved_registry_states: + # Restore the old Registry object's state + registry, reglist = self.saved_registry_states[request_id] + registry.reglist = reglist + + self.restoration_context_id.request_id = request_id + + def restoration_end(self): + """Register a restoration context as finished, if one exists""" + try: + del self.restoration_context_id.request_id + except AttributeError: + pass + + def in_restoration(self): + """Determine if a restoration context is active for the current thread. + Returns the request_id it's active for if so, otherwise False""" + return getattr(self.restoration_context_id, 'request_id', False) + +restorer = StackedObjectRestorer() + + +# Paste Deploy entry point +def make_registry_manager(app, global_conf): + return RegistryManager(app) + +make_registry_manager.__doc__ = RegistryManager.__doc__ -- cgit v1.2.1