Commits

holger krekel committed 21d5a19

initial implementation

  • Participants

Comments (0)

Files changed (9)

+
+# Automatically generated by `hgimportsvn`
+syntax:glob
+.svn
+.hgsvn
+
+# These lines are suggested according to the svn:ignore property
+# Feel free to enable them by uncommenting them
+syntax:glob
+*.pyc
+*.pyo
+*.swp
+*.html
+*.class
+*.orig
+*~
+
+doc/*/_build
+build/
+dist/
+*.egg-info
+issue/
+env/
+3rdparty/
+.tox
+.cache
+.coverage
+
+0.5 (initial version)
+-------------------------------------
+
+- offer xlocal.xlocal() to manage execution locals
+
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
+     
+  The above copyright notice and this permission notice shall be included in all
+  copies or substantial portions of the Software.
+ 
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+  SOFTWARE.
+
+include CHANGELOG
+include README.txt
+include setup.py
+include test_xlocal.py
+include tox.ini
+include LICENSE
+
+execution locals: killing globals state (including thread locals)
+===================================================================
+
+This module provides execution locals aka "xlocal" objects which implement
+a more restricted variant of "thread locals".  An execution local allows to
+manage attributes on a per-execution basis in a manner similar to how real
+locals work:
+
+- Invoked functions cannot change the binding for the invoking function
+- life-cycle is enforced by only allowing bindings to be set through a with-statement
+
+Attribute bindings for an xlocal objects will not leak outside a
+context-managed code block and they will not leak to other threads of
+greenlets.  By contrast, both process-globals and so called "thread
+locals" do not implement the above properties.
+
+Let's look at a basic example::
+
+    # content of example.py
+
+    from xlocal import xlocal
+
+    xcurrent = xlocal()
+
+    def output():
+        print "hello world", xcurrent.x
+
+    if __name__ == "__main__":
+        with xcurrent(x=1):
+            output()
+
+If we execute this module, the ``output()`` function will see 
+a ``xcurrent.x==1`` binding::
+
+    $ python example.py
+    hello world 1
+
+Here is what happens in detail: ``xcurrent(x=1)`` returns a context manager which 
+sets/resets the ``x`` attribute on the ``xcurrent`` object.  While remaining
+in the same thread/greenlet, all code triggered by the with-body (in this case
+just the ``output()`` function) can access ``xcurrent.x``.  Outside the with-
+body ``xcurrent.x`` would raise an AttributeError.  It is also not allowed
+to directly set ``xcurrent`` attributes; you always have to explicitely mark their
+life-cycle with a with-statement.  This means that invoked code:
+
+- cannot rebind xlocal state of its invoking functions (no side effects, yay!)
+- xlocal state does not leak outside the with-context (lifecylcle control)
+
+Another module may now reuse the example code::
+
+    # content of example_call.py
+    import example
+    
+    with example.xcurrent(x=3):
+        example.output()
+
+which when running ...::
+
+    $ python example_call.py
+    hello world 3
+
+will cause the ``example.output()`` function to print the ``xcurrent.x`` binding
+as defined at the invoking ``with xcurrent(x=42)`` statement.
+
+Other threads or greenlets will never see this ``xcurrent.x`` binding; they may even 
+set and read their own distincit ``xcurrent.x`` object.  This means that all 
+threads/greenlets can concurrently call into a function which will always
+see the execution specific ``x`` attribute.
+
+Usage in frameworks and libraries invoking "handlers"
+-----------------------------------------------------------
+
+When invoking plugin code or handler code to perform work, you may not
+want to pass around all state that might ever be needed.  Instead of using
+a global or thread local you can safely pass around such state in 
+execution locals. Here is a pseudo example::
+
+    xcurrent = xlocal.new()
+
+    def with_xlocal(func, **kwargs):
+        with xcurrent(**kwargs):
+            func()
+
+    def handle_request(request):
+        func = gethandler(request)  # some user code
+        spawn(with_xlocal(func, request=request))
+
+``handle_request`` will run a user-provided handler function in a newly
+spawned execution unit (for example spawn might map to
+``threading.Thread(...).start()`` or to ``gevent.spawn(...)``).  The
+generic ``with_xlocal`` helper wraps the execution of the handler
+function so that it will see a ``xcurrent.request`` binding.  Multiple
+spawns may execute concurrently and ``xcurrent.request`` will
+carry the execution-specific request object in each of them.
+
+
+Issues worth noting
+---------------------------------------
+
+If a method decides to store an attribute of an execution local, for
+example the above ``xcurrent.request`` then it will keep a reference to
+the exact request object, not the per-execution one.  If you want to
+keep a per-execution local, you can do it this way for example::
+
+    Class Renderer:
+        @property
+        def request(self):
+            return xcurrent.request
+
+this means that Renderer instances will have an execution-local
+``self.request`` object even if the life-cycle of the instance crosses
+execution units.
+
+Another issue is that if you spawn new execution units, they will not 
+implicitely inherit execution locals.  Instead you have to wrap
+your spawning function to explicitely set execution locals, similar to
+what we did in the above "invoking handlers" section.
+
+Copyright / inspiration
+-------------------------------------
+
+This code is based on discussions with Armin Ronacher and others
+in response to a `tweet of mine <https://twitter.com/hpk42/status/268012251888353280>`_. It extracts and refines some ideas found in Armin's "werzeug.local" module
+and friends.
+
+:copyright: (c) 2012 by Holger Krekel, partially Armin Ronacher
+:license: BSD, see LICENSE for more details.
+from setuptools import setup
+
+if __name__ == "__main__":
+    setup(
+        name='xlocal',
+        description='execution locals: killing global state (including thread locals)',
+        long_description=open("README.txt").read(),
+        version='0.5',
+        author='Holger Krekel',
+        author_email='holger.krekel@gmail.com',
+        url='http://bitbucket.org/hpk42/xlocal/',
+        py_modules=['xlocal', "test_xlocal"],
+        #install_requires=['pytest-cache', 'pytest>=2.3.dev14', 'pep8>=1.3', ],
+    )
+
+from __future__ import with_statement
+
+import pytest
+
+@pytest.fixture
+def xlocal(request):
+    import xlocal
+    return xlocal.xlocal()
+
+def test_scoping(xlocal):
+    def f():
+        assert xlocal.x == 3
+
+    with xlocal(x=3):
+        f()
+    assert not hasattr(xlocal, "x")
+    assert not xlocal._storage # no garbage
+
+def test_stacking(xlocal):
+    def f():
+        assert xlocal.x == 3
+
+    with xlocal(x=5):
+        assert xlocal.x == 5
+        with xlocal(x=3):
+            assert xlocal.x == 3
+        assert xlocal.x == 5
+    assert not hasattr(xlocal, "x")
+
+def test_nodelset(xlocal):
+    with xlocal(y=3):
+        assert xlocal.y == 3
+        pytest.raises(AttributeError, lambda: delattr(xlocal, "y"))
+    assert not hasattr(xlocal, "y")
+    pytest.raises(AttributeError, lambda: setattr(xlocal, "x", None))
+
+[tox]
+envlist=py25,py26,py27,py32,py33,pypy
+indexserver=
+    pypi = http://pypi.python.org/simple
+    testrun = http://pypi.testrun.org
+    #default = http://pypi.testrun.org
+
+[testenv]
+commands= py.test
+deps=pytest
+
+# -*- coding: utf-8 -*-
+"""
+
+This module helps to kill global state in your program
+and replace it with execution-bound state which preserves
+a lot of the good properties of using true locals.
+
+This module uses a bit of code from "werkzeug.local" by Armin Ronacher.
+
+:copyright: (c) 2012 by Holger Krekel, partially Armin Ronacher
+:license: BSD, see LICENSE for more details.
+"""
+
+# use since each thread has its own greenlet we can just use those as identifiers
+# for the context.  If greenlets are not available we fall back to the
+# current thread ident.
+
+try:
+    from greenlet import getcurrent as _getident
+except ImportError:
+    try:
+        from thread import get_ident as _getident
+    except ImportError:
+        try:
+            from _thread import get_ident as _getident
+        except ImportError:
+            from dummy_thread import get_ident as _getident
+
+class xlocal(object):
+    """ Implementation of an execution local object. """
+    def __init__(self):
+        d = self.__dict__
+        d["_getident"] = _getident
+        d["_storage"] = {}
+
+    def _getlocals(self, autocreate=False):
+        ident = self._getident()
+        try:
+            return self._storage[ident]
+        except KeyError:
+            if not autocreate:
+                raise
+            self._storage[ident] = loc = {}
+            return loc
+
+    def _checkremove(self):
+        ident = self._getident()
+        val = self._storage.get(ident)
+        if val is not None:
+            if not val:
+                del self._storage[ident]
+
+    def __call__(self, **kwargs):
+        """ return context manager which will set execution locals
+        for all code within the with-body.
+        """
+        return WithXLocals(self, kwargs)
+
+    def __getattr__(self, name):
+        try:
+            return self._getlocals()[name]
+        except KeyError:
+            raise AttributeError(name)
+
+    def __setattr__(self, name, val):
+        raise AttributeError("cannot setattr, use 'with scoping'")
+
+    def __delattr__(self, name):
+        raise AttributeError("cannot delattr xlocal local attr")
+
+class WithXLocals:
+    def __init__(self, xlocal, kwargs):
+        self._xlocal = xlocal
+        self._kwargs = kwargs
+
+    def __enter__(self):
+        loc = self._xlocal._getlocals(autocreate=True)
+        self._undostack = undostack = []
+        for name, val in self._kwargs.items():
+            assert name[0] != "_", "names with underscore reserved for future use"
+            try:
+                undostack.append(lambda n=name,v=loc[name]: loc.__setitem__(n, v))
+            except KeyError:
+                undostack.append(lambda n=name: loc.__delitem__(n))
+            loc[name] = val
+        return self
+
+    def __exit__(self, *args):
+        for action in self.__dict__.pop("_undostack"):
+            action()
+        self._xlocal._checkremove()