Hack python stack frames to provide additional nested scopes via the managed interface's with keyword
Python C

README.md

Overview of python-withscope

Build Status Coverage Status

This project embodies a completely mad idea: create a working let syntax for Python to provide nested lexical scopes beyond the existing global/local scopes. It should be regarded as a freak of nature and not used for anything serious. I created this project because I was pretty sure I could do so, but I wanted to be certain. Also, I get a real buzz when something like this works, and throwing back my head with screams of laughter is my chicken soup for the soul.

Using withscope

from withscope import let

a = "taco"
with let(a="pizza", b="beer"):
    print "%s and %s" % (a, b) # >>> "pizza and beer"

print a # >>> "taco"
print b # >>> NameError: name 'b' is not defined

You can also save scopes and re-enter them whenever you feel like it.

from withscope import let

with let(a="pizza", b="beer") as my_scope:
    print "actually, not that hungry right now"
    a = "popcorn"
    b = "water"

print a # >>> NameError: name 'a' is not defined

with my_scope:
    print "%s and %s" % (a, b) # >>> "popcorn and water"

Yes really, that works. It will correctly fall-through to outer scopes as well

from withscope import let

monster = "godzilla"
city = "Tokyo"

print "%s is attacking %s" % (monster, city)
# >>> "godzilla is attacking Tokyo"

with let(monster="mothra"):
    print "%s is attacking %s" % (monster, city)
    # >>> "mothra is attacking Tokyo"

    city = "New York"

print "%s is attacking %s" % (monster, city)
# >>> "godzilla is attacking New York"

It also functions correctly with closures, giving you a new set of cells to capture while you're inside a new scope, then returning the original cells to their place when the scope ends.

The Story

I wanted some way to push and subsequently pop a lexical scope in Python. It's not a Pythonic thing to want, but that's fancy for you.

The managed interface looked very promising; the with keyword visually identified a region of code, and had hooks to setup and tear-down environmental changes, even in the event of exceptions.

My original experiments with this fell apart. I wanted to do the work in pure Python, but rewriting locals and globals was just a mess. It would require a native extension to swap those fields of the call frame.

Finally Getting Around To It

It was many months later that I was awake one night and thought, "hey, let's give it a serious shot!" I hacked together a Scope class that would fetch locals, wrap it into a layered dict with the user-specified scope bindings, and stuff it into the frame and back again.

This was good enough for very simple cases, but broke whenever there was a new variable that hadn't already been defined. I solved this case by also embedding lexical vars into a layered globals. Any read references would fall through and fetch the correct value. Any write references would be a local assignment. At the end of the scope, the layered globals could be discarded as useless.

I encountered a few issues. For example, when creating a closure, the cell vars are harvested pre-created from the frame. This meant that closures would revert to the original value when the scope closed, even if the closure was captured inside the new scope. I got around this by re-creating the appropriate cells when pushing a new scope, but with the same values. There was also an issue with how the del statement behaved, which caused me a great deal of frustration and eventually forced me to accept less-than-ideal behavior as expected. I wanted del of a lexical name to cause later references to fall-through to any prior definition. But there is no way for me to hook additional behavior to that statement at the point it occurs! I could detect it later, when for examples locals() would be called. I had to abandon fall-through and just accept that a del of a lexical name from within a scope would cause the name to be undefined until the scope exited.

What's Next?

I've learned a lot about the many ways that Python stores runtime variables. They're all over the place. I've also learned some interesting things about frames, the allocator, and some of the bytecode implementation details. I'd like to take all of that and write it up for educational purposes, perhaps as a blog entry.

I have a few things I might like to scoot over into the extension and out of the python module, but they work as-is.

Requirements

  • Python 2.6 or later (no support for Python 3, the underlying function fields differ a bit)

In addition, the following tools are used in building, testing, or generating documentation from the project sources.

These are all available in most linux distributions (eg. Fedora), and for OSX via MacPorts.

Building

This module uses setuptools, so simply run the following to build the project.

python setup.py build

Testing

Tests are written as unittest test cases. If you'd like to run the tests, simply invoke:

python setup.py test

You may check code coverage via coverage.py, invoked as:

# generates coverage data in .coverage
coverage run --source=withscope setup.py test

# creates an html report from the above in htmlcov/index.html
coverage html

I've setup travis-ci and coveralls.io for this project, so tests are run automatically, and coverage is computed then. Results are available online:

TODO

  • type-checking in the withscope._frame extension
  • Is a documentation branch worthwhile?
  • Is a Python 3 branch worthwhile?
  • write more examples, eg. depicting the use of Scope.alias()
  • a scope that references an object's attributes via getattr (for use with something like the option object from optparser)
  • profiling and optimizations? Do I care?

Author

Christopher O'Brien obriencj@gmail.com

If this project interests you, you can read about more of my hacks and ideas on on my blog.

License

This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License along with this library; if not, see http://www.gnu.org/licenses/.