Commits

Jason Baker committed 4cef33e

Adding utility functions

Comments (0)

Files changed (11)

-See the documentation at http://packages.python.org/pysistence/ for more info.
+Pysistence is a project that seeks to make functional programming in python easier.
+
+FAQ
+~~~~~
+
+**Why the name pysistence?**
+
+The project is named pysistence because most of the data structures it
+implements are *persistent*.  This doesn't mean persistent in the sense that
+they are stored in a database or the file system.  Rather, it means the data
+structures may only be modified by copying.  Where possible, these data
+structures will re-use existing implementations without copying everything.
+
+**Where can I learn more about persistent data structures?**
+
+Here are some resouces:
+
+ * `Developing for Developers:  Persistent data structures <http://blogs.msdn.com/devdev/archive/2005/11/08/490480.aspx>`_
+ * `Persistent data structures (wikipedia) <http://en.wikipedia.org/wiki/Persistent_data_structure>`_
+
+Or if you're *really* brave:
+
+ * `Making Data Structures Persistent <http://www.cs.cmu.edu/~sleator/papers/another-persistence.pdf>`_
+ * `Purely Functional Data Structures <http://www.cs.cmu.edu/~rwh/theses/okasaki.pdf>`_
+
+
+Download
+=========
+
+You may download the current release of Pysistence from our `PyPI page <http://pypi.python.org/pypi/pysistence/>`_.  You may also use easy_install to install pysistence::
+
+    sudo easy_install pysistence
+
+If you have Mercurial installed, you can use the following command to check out Pysistence::
+
+    hg clone http://bitbucket.org/jasbaker/pysistence/
+
+Note that you do need `setuptools <http://pypi.python.org/pypi/setuptools>` or `distribute <http://pypi.python.org/pypi/distribute>` for the next step::
+
+    python setup.py install
+
+If you're brave, you can try using pysistence's experimental C extension::
+
+    python setup.py install --with-cext
+
+
+Developers & Community
+=======================
+
+Visit `Pysistence's bitbucket page <http://bitbucket.org/jasbaker/pysistence/>`_
+to get more info and view the wiki.
+
+You may also visit our `Google Group <http://groups.google.com/group/pysistence>`_ for more info.

doc/source/conf.py

 
 # General information about the project.
 project = u'Pysistence'
-copyright = u'2009, Jason Baker'
+copyright = u'2009-2011, Jason Baker'
 
 # The version info for the project you're documenting, acts as replacement for
 # |version| and |release|, also used in various other places throughout the
 # built documents.
 #
 # The short X.Y version.
-version = '0.3'
+version = '0.4'
 # The full version, including alpha/beta/rc tags.
-release = '0.3.0a1'
+release = '0.4.0'
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
 
 # The theme to use for HTML and HTML Help pages.  Major themes that come with
 # Sphinx are currently 'default' and 'sphinxdoc'.
-html_theme = 'sphinxdoc'
+html_theme = 'agogo'
 
 # Theme options are theme-specific and customize the look and feel of a theme
 # further.  For a list of options available for each theme, see the

doc/source/index.rst

 Introduction
 ~~~~~~~~~~~~~
 
-Pysistence is a project that seeks to bring functional data structures to
-Python.  It is currently very much under development, but keep checking back!
+Pysistence is a project that seeks to make functional programming in python easier.
 
 .. toctree::
 
    pysistence/persistent_list
    pysistence/expando
    pysistence/persistent_dict
+   pysistence/func
 
 FAQ
 ~~~~~
 
     hg clone http://bitbucket.org/jasbaker/pysistence/
 
-If you check out pysistence from source, you will need `paver <http://www.blueskyonmars.com/projects/paver/>`_ to install pysistence.  If you have setuptools, you may install it using easy_install::
+Note that you do need `setuptools <http://pypi.python.org/pypi/setuptools>`_ or `distribute <http://pypi.python.org/pypi/distribute>`_ for the next step::
 
-    sudo easy_install Paver
+    python setup.py install
 
-Once you have paver installed, you should use the following commands::
+If you're brave, you can try using pysistence's experimental C extension::
 
-    cd $DIR_YOU_PUT_PYSISTENCE_IN
-    sudo paver install
+    python setup.py install --with-cext
 
-If you're running windows, then you should omit "sudo" in each of the above 
-commands.
 
 Developers & Community
 =======================

doc/source/pysistence/func.rst

+Functional utilities
+------------------------
+
+.. module:: pysistence.func
+   :synopsis: Common functional utilities
+
+.. function:: flip(func)
+
+   Return a function that is equivalent to *func* except that it has its first two
+   arguments reversed.  For instance ``flip(my_function)(a, b)`` is equivalent to
+   ``my_function(b, a)``.
+
+.. function:: const(retval)
+
+   Return a function that will always return *retval*.
+
+.. function:: compose(func1, *funcs)
+
+   Create a function that composes *func1* and *funcs* from left to right.
+   For example::
+
+       >>> from functools import partial
+       >>> from pysistence.func import compose
+       >>> rejoin = compose(str.split, partial(str.join, '-'))
+       >>> rejoin("This is a string")
+       'This-is-a-string'
+
+.. function:: identity(*args)
+
+   If a single argument is given, return it.  Otherwise, return a tuple of the
+   arguments given.  Example::
+
+       >>> from pysistence.func import identity
+       >>> identity(1)
+       1
+       >>> identity(1, 2, 3)
+       (1, 2, 3)
+       >>> identity()
+       ()
+
+.. function:: trampoline(func, *args, **kwargs)
+
+   Calls func with *args* and *kwargs*.  If the result is a callable, it will 
+   call that function and repeat this process until a non-callable is returned.
+   This can be useful to write recursive functions without blowing the stack.
+   Example::
+
+       >>> from functools import partial
+       >>> from pysistence.func import trampoline
+       >>> def count_to_a_million(i):
+       ...     i += 1
+       ...     if i < 1000000:
+       ...             return partial(count_to_a_million, i)
+       ...     else:
+       ...             return i
+       ... 
+       >>> trampoline(count_to_a_million, 1)
+       1000000
+# Technically, we don't need this in python 2.6+, but it isn't hurting anything.
+from __future__ import with_statement
+
 import sys
 import tarfile
 from tempfile import mkdtemp
                       ['source/pysistence/_persistent_list.c'], ))
     sys.argv.remove('--with-cext')
 
+with open("README") as README:
+    # See?  The installer reads the README for the user!
+    README_text = README.read()
+
 setup(
     name=name,
     packages=find_packages('source'),
     version=version,
     url="http://packages.python.org/pysistence",
     description='A set of functional data structures for Python',
-    long_description='Pysistence is a library that seeks to bring functional data structures to Python.',
+    long_description=README_text,
     author="Jason Baker",
     author_email="amnorvend@gmail.com",
     test_suite = 'nose.collector',

source/pysistence/expando.py

-# Copyright (c) 2009 Jason M Baker
-#
-# 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.
-"""
-This module contains the Expando class.
-"""
-
-from copy import copy
-from pprint import pformat
-
-def make_expando_class(base, name, **kwargs):
-    """This function defines an 'expando' class.  An expando class is one that
-       allows arbitrary properties to be defined.  To use it, you would want to
-       pass in a base class, a name, and arbitrary keyword arguments that define
-       properties on the class.  For example, this call:
-
-           ExpandoSomeClass = make_expando_class(SomeClass, 'ExpandoSomeClass,
-                                                 x=1, y=2)
-       ...is equivalent to the following statement:
-
-           class ExpandoSomeClass(SomeClass):
-               x = 1
-               y = 2
-               def __init__(self, **kwargs):
-                   self.__dict__.update(**kwargs)
-
-       Expando classes also allow arbitrary keyworks arguments in constructor
-       calls.  For example, the following would set expando.mbr_id equal to 32
-       and expando.store to an object:
-
-           expando = ExpandoSomeClass(mbr_id=32, store=object())
-    """
-
-    kwargs['__init__'] = init_expando
-    kwargs['to_dict'] = expando_to_dict
-    kwargs['without'] = expando_without
-    kwargs['using'] = expando_using
-    kwargs['to_public_dict'] = expando_to_public_dict
-    kwargs['__repr__'] = expando_repr
-    return type(name, (base,), kwargs)
-
-# stub for backwards compatibility
-mkExpandoClass = make_expando_class
-
-def init_expando(self, **instance_kwargs_):
-    self.__dict__.update(**instance_kwargs_)
-
-def expando_to_dict(self):
-    """
-    Get a *copy* of the expando's __dict__.
-    """
-    original_dict = self.__dict__
-    return copy(original_dict)
-
-def expando_to_public_dict(self):
-    """
-    Get a *copy* of the expando's __dict__ without any keys
-    beginning with an underscore
-    """
-    new_dict = self.to_dict()
-    filtered_dict = dict(
-        [(key, value) for key, value in new_dict.iteritems()
-             if not key.startswith('_')])
-    return filtered_dict
-
-def expando_without(self, *args):
-    """
-    Make a copy of an expando without certain attributes
-    """
-    new_dict = self.to_dict()
-    for attr_name in args:
-        del new_dict[attr_name]
-    return self.__class__(**new_dict)
-
-def expando_using(self, **kwargs):
-    """
-    Create a new copy of an expando with additional attributes.
-    """
-    new_dict = self.to_dict()
-    new_dict.update(kwargs)
-    return self.__class__(**new_dict)    
-
-def expando_repr(self):
-    return pformat(self.__dict__)
-
-Expando = make_expando_class(object, 'Expando')
+# Copyright (c) 2009 Jason M Baker
+#
+# 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.
+"""
+This module contains the Expando class.
+"""
+
+from copy import copy
+from pprint import pformat
+
+def make_expando_class(base, name, **kwargs):
+    """This function defines an 'expando' class.  An expando class is one that
+       allows arbitrary properties to be defined.  To use it, you would want to
+       pass in a base class, a name, and arbitrary keyword arguments that define
+       properties on the class.  For example, this call:
+
+           ExpandoSomeClass = make_expando_class(SomeClass, 'ExpandoSomeClass,
+                                                 x=1, y=2)
+       ...is equivalent to the following statement:
+
+           class ExpandoSomeClass(SomeClass):
+               x = 1
+               y = 2
+               def __init__(self, **kwargs):
+                   self.__dict__.update(**kwargs)
+
+       Expando classes also allow arbitrary keyworks arguments in constructor
+       calls.  For example, the following would set expando.mbr_id equal to 32
+       and expando.store to an object:
+
+           expando = ExpandoSomeClass(mbr_id=32, store=object())
+    """
+
+    kwargs['__init__'] = init_expando
+    kwargs['to_dict'] = expando_to_dict
+    kwargs['without'] = expando_without
+    kwargs['using'] = expando_using
+    kwargs['to_public_dict'] = expando_to_public_dict
+    kwargs['__repr__'] = expando_repr
+    return type(name, (base,), kwargs)
+
+# stub for backwards compatibility
+mkExpandoClass = make_expando_class
+
+def init_expando(self, **instance_kwargs_):
+    self.__dict__.update(**instance_kwargs_)
+
+def expando_to_dict(self):
+    """
+    Get a *copy* of the expando's __dict__.
+    """
+    original_dict = self.__dict__
+    return copy(original_dict)
+
+def expando_to_public_dict(self):
+    """
+    Get a *copy* of the expando's __dict__ without any keys
+    beginning with an underscore
+    """
+    new_dict = self.to_dict()
+    filtered_dict = dict(
+        [(key, value) for key, value in new_dict.items()
+             if not key.startswith('_')])
+    return filtered_dict
+
+def expando_without(self, *args):
+    """
+    Make a copy of an expando without certain attributes
+    """
+    new_dict = self.to_dict()
+    for attr_name in args:
+        del new_dict[attr_name]
+    return self.__class__(**new_dict)
+
+def expando_using(self, **kwargs):
+    """
+    Create a new copy of an expando with additional attributes.
+    """
+    new_dict = self.to_dict()
+    new_dict.update(kwargs)
+    return self.__class__(**new_dict)    
+
+def expando_repr(self):
+    return pformat(self.__dict__)
+
+Expando = make_expando_class(object, 'Expando')

source/pysistence/func.py

+"""
+A set of common functional programming utility functions.
+"""
+
+from functools import wraps, partial
+
+def flip(func):
+    """Returns a function that calls func flipping its first two arguments.
+       Note that the returned function will not accept keyword arguments."""
+    @wraps(func)
+    def wrapper(arg1, arg2, *args):
+        return func(arg2, arg1, *args)
+    return wrapper
+
+def const(retval):
+    """Returns a function that always returns the same value, no matter what
+       arguments it is given."""
+    def constfunc(*args, **kwargs):
+        return retval
+    return constfunc
+
+def compose(func1, *funcs):
+    """Compose several functions together."""
+    class ComposedFunc(object):
+        _funcs = (func1,) + funcs
+        __doc__ = "Function composed from {0}".format(_funcs)
+
+        def __call__(self, *args, **kwargs):
+            result = func1(*args, **kwargs)
+            for func in funcs:
+                result = func(result)
+            return result
+
+        def __repr__(self):
+            return "<function composed from {0}>".format(self._funcs)
+    return ComposedFunc()
+
+def identity(*args):
+    """Function that returns what is passed in.  If one item is given, that item
+       will be returned.  Otherwise, a tuple of the arguments will be passed in."""
+    if len(args) == 1:
+        return args[0]
+    else:
+        return args
+
+def trampoline(func, *args, **kwargs):
+    """Calls func with the given arguments.  If func returns another function,
+       it will call that function and repeat this process until a non-callable
+       is returned."""
+    result = func(*args, **kwargs)
+    while callable(result):
+        result = result()
+    return result

source/pysistence/persistent_dict.py

 # THE SOFTWARE.
 
 def not_implemented_method(*args, **kwargs):
-    raise NotImplementedError, 'Cannot set values in a PDict'    
+    raise NotImplementedError('Cannot set values in a PDict')    
 
 class PDict(dict):
     __setitem__ = not_implemented_method

source/pysistence/persistent_list.py

             """
             return self._rest
 
-    print 'Not using C'
+    print('Not using C')
 
 
 def make_list(items):
         Concatenate this list with another list.
         """
         reversed_self = reversed(self)
-        print reversed_self
+        print(reversed_self)
         new_list = next_list
         for item in reversed_self:
             new_list = new_list.cons(item)
                 item = item.rest
         else:
             msg = 'Item %s not found in list' % old
-            raise ItemNotFoundError, msg
+            raise ItemNotFoundError(msg)
 
     def reverse(self):
         """
         cmp_iterator = [item1 == item2 for (item1, item2) in zipped_iter]
         return all(cmp_iterator)
 
-    def __nonzero__(self):
+    def __bool__(self):
         return True
 
     __add__ = concat
     def __iter__(self):
         return iter(())
 
-    def __nonzero__(self):
+    def __bool__(self):
         return False

source/pysistence/tests/test_func.py

+import unittest
+import functools
+import operator
+import pysistence.func as func
+
+class TestFlip(unittest.TestCase):
+    def test_flip(self):
+        def myfunc(a, b):
+            return (a, b)
+        result = func.flip(myfunc)(1, 2)
+        self.assertEqual(result, (2, 1))
+
+    def test_flip_with_more_args(self):
+        def myfunc(a, b, c):
+            return a, b, c
+        result = func.flip(myfunc)(1, 2, 3)
+        self.assertEqual(result, (2, 1, 3))
+
+class TestConst(unittest.TestCase):
+    def test_const(self):
+        myfunc = func.const(1)
+        # pass in arbitrary arguments just to make sure we can handle those
+        self.assertEqual(myfunc(1, 2, foo="bar"), 1)
+
+class TestCompose(unittest.TestCase):
+    def setUp(self):
+        self.add_to_hello = functools.partial(operator.add, "Hello, ")
+
+    def test_compose_twoarg(self):
+        hellocat = func.compose(str, self.add_to_hello)
+        self.assertEqual(hellocat(1), "Hello, 1")
+
+    def test_compose_threearg(self):
+        append_exclamation_point = (functools.partial(func.flip(operator.add), "!"))
+        make_hello = func.compose(str, self.add_to_hello, append_exclamation_point)
+        self.assertEqual(make_hello(1), "Hello, 1!")
+
+class TestIdentity(unittest.TestCase):
+    def test_identity_one_arg(self):
+        result = func.identity(1)
+        self.assertEqual(result, 1)
+
+    def test_identity_multi_arg(self):
+        result = func.identity(1, 2)
+        self.assertEqual(result, (1, 2))
+
+    def test_identity_no_arg(self):
+        result = func.identity()
+        self.assertEqual(result, ())
+
+class TestTrampoline(unittest.TestCase):
+    def test_no_func(self):
+        # Test that if no function is given, trampoline is basically a glorified
+        # apply.
+        def myfunc():
+            return 1
+        self.assertEqual(func.trampoline(myfunc), 1)
+
+    def test_with_other_functions(self):
+        def myfunc1():
+            return myfunc2
+
+        def myfunc2():
+            return myfunc3
+
+        def myfunc3():
+            return 1
+
+        self.assertEqual(func.trampoline(myfunc1), 1)
+
+    def test_with_args(self):
+        def myfunc(a):
+            return a
+        self.assertEqual(func.trampoline(myfunc, 1), 1)

source/pysistence/util.py

 try:
     from itertools import izip_longest
 except ImportError:
-    from itertools import izip, chain, repeat
+    from itertools import chain, repeat
     def izip_longest(*args, **kwds):
         # izip_longest('ABCD', 'xy', fillvalue='-') --> Ax By C- D-
         fillvalue = kwds.get('fillvalue')
         fillers = repeat(fillvalue)
         iters = [chain(it, sentinel(), fillers) for it in args]
         try:
-            for tup in izip(*iters):
+            for tup in zip(*iters):
                 yield tup
         except IndexError:
             pass