[Python-checkins] r79810 - in python/branches/py3k: Doc/library/functions.rst Doc/library/functools.rst Doc/library/stdtypes.rst Doc/reference/datamodel.rst Lib/functools.py Lib/pstats.py Lib/test/test_functools.py Lib/unittest/loader.py Lib/unittest/util.py Misc/NEWS

raymond.hettinger python-checkins at python.org
Mon Apr 5 20:56:31 CEST 2010


Author: raymond.hettinger
Date: Mon Apr  5 20:56:31 2010
New Revision: 79810

Log:
Forward port total_ordering() and cmp_to_key().

Modified:
   python/branches/py3k/Doc/library/functions.rst
   python/branches/py3k/Doc/library/functools.rst
   python/branches/py3k/Doc/library/stdtypes.rst
   python/branches/py3k/Doc/reference/datamodel.rst
   python/branches/py3k/Lib/functools.py
   python/branches/py3k/Lib/pstats.py
   python/branches/py3k/Lib/test/test_functools.py
   python/branches/py3k/Lib/unittest/loader.py
   python/branches/py3k/Lib/unittest/util.py
   python/branches/py3k/Misc/NEWS

Modified: python/branches/py3k/Doc/library/functions.rst
==============================================================================
--- python/branches/py3k/Doc/library/functions.rst	(original)
+++ python/branches/py3k/Doc/library/functions.rst	Mon Apr  5 20:56:31 2010
@@ -1014,9 +1014,8 @@
    *reverse* is a boolean value.  If set to ``True``, then the list elements are
    sorted as if each comparison were reversed.
 
-   To convert an old-style *cmp* function to a *key* function, see the
-   `CmpToKey recipe in the ASPN cookbook
-   <http://code.activestate.com/recipes/576653/>`_\.
+   Use :func:`functools.cmp_to_key` to convert an
+   old-style *cmp* function to a *key* function.
 
    For sorting examples and a brief sorting tutorial, see `Sorting HowTo
    <http://wiki.python.org/moin/HowTo/Sorting/>`_\.

Modified: python/branches/py3k/Doc/library/functools.rst
==============================================================================
--- python/branches/py3k/Doc/library/functools.rst	(original)
+++ python/branches/py3k/Doc/library/functools.rst	Mon Apr  5 20:56:31 2010
@@ -15,6 +15,51 @@
 
 The :mod:`functools` module defines the following functions:
 
+..  function:: cmp_to_key(func)
+
+    Transform an old-style comparison function to a key-function.  Used with
+    tools that accept key functions (such as :func:`sorted`, :func:`min`,
+    :func:`max`, :func:`heapq.nlargest`, :func:`heapq.nsmallest`,
+    :func:`itertools.groupby`).
+    This function is primarily used as a transition tool for programs
+    being converted from Py2.x which supported the use of comparison
+    functions.
+
+    A compare function is any callable that accept two arguments, compares
+    them, and returns a negative number for less-than, zero for equality,
+    or a positive number for greater-than.  A key function is a callable
+    that accepts one argument and returns another value that indicates
+    the position in the desired collation sequence.
+
+    Example::
+
+        sorted(iterable, key=cmp_to_key(locale.strcoll))  # locale-aware sort order
+
+   .. versionadded:: 3.2
+
+.. function:: total_ordering(cls)
+
+   Given a class defining one or more rich comparison ordering methods, this
+   class decorator supplies the rest.  This simplies the effort involved
+   in specifying all of the possible rich comparison operations:
+
+   The class must define one of :meth:`__lt__`, :meth:`__le__`,
+   :meth:`__gt__`, or :meth:`__ge__`.
+   In addition, the class should supply an :meth:`__eq__` method.
+
+   For example::
+
+       @total_ordering
+       class Student:
+           def __eq__(self, other):
+               return ((self.lastname.lower(), self.firstname.lower()) ==
+                       (other.lastname.lower(), other.firstname.lower()))
+           def __lt__(self, other):
+               return ((self.lastname.lower(), self.firstname.lower()) <
+                       (other.lastname.lower(), other.firstname.lower()))
+
+   .. versionadded:: 3.2
+
 .. function:: partial(func, *args, **keywords)
 
    Return a new :class:`partial` object which when called will behave like *func*

Modified: python/branches/py3k/Doc/library/stdtypes.rst
==============================================================================
--- python/branches/py3k/Doc/library/stdtypes.rst	(original)
+++ python/branches/py3k/Doc/library/stdtypes.rst	Mon Apr  5 20:56:31 2010
@@ -1567,6 +1567,9 @@
 
    *key* specifies a function of one argument that is used to extract a comparison
    key from each list element: ``key=str.lower``.  The default value is ``None``.
+   Use :func:`functools.cmp_to_key` to convert an
+   old-style *cmp* function to a *key* function.
+
 
    *reverse* is a boolean value.  If set to ``True``, then the list elements are
    sorted as if each comparison were reversed.

Modified: python/branches/py3k/Doc/reference/datamodel.rst
==============================================================================
--- python/branches/py3k/Doc/reference/datamodel.rst	(original)
+++ python/branches/py3k/Doc/reference/datamodel.rst	Mon Apr  5 20:56:31 2010
@@ -1209,8 +1209,7 @@
    Arguments to rich comparison methods are never coerced.
 
    To automatically generate ordering operations from a single root operation,
-   see the `Total Ordering recipe in the ASPN cookbook
-   <http://code.activestate.com/recipes/576529/>`_\.
+   see :func:`functools.total_ordering`.
 
 .. method:: object.__hash__(self)
 

Modified: python/branches/py3k/Lib/functools.py
==============================================================================
--- python/branches/py3k/Lib/functools.py	(original)
+++ python/branches/py3k/Lib/functools.py	Mon Apr  5 20:56:31 2010
@@ -49,3 +49,50 @@
     """
     return partial(update_wrapper, wrapped=wrapped,
                    assigned=assigned, updated=updated)
+
+def total_ordering(cls):
+    'Class decorator that fills-in missing ordering methods'
+    convert = {
+        '__lt__': [('__gt__', lambda self, other: other < self),
+                   ('__le__', lambda self, other: not other < self),
+                   ('__ge__', lambda self, other: not self < other)],
+        '__le__': [('__ge__', lambda self, other: other <= self),
+                   ('__lt__', lambda self, other: not other <= self),
+                   ('__gt__', lambda self, other: not self <= other)],
+        '__gt__': [('__lt__', lambda self, other: other > self),
+                   ('__ge__', lambda self, other: not other > self),
+                   ('__le__', lambda self, other: not self > other)],
+        '__ge__': [('__le__', lambda self, other: other >= self),
+                   ('__gt__', lambda self, other: not other >= self),
+                   ('__lt__', lambda self, other: not self >= other)]
+    }
+    roots = set(dir(cls)) & set(convert)
+    assert roots, 'must define at least one ordering operation: < > <= >='
+    root = max(roots)       # prefer __lt __ to __le__ to __gt__ to __ge__
+    for opname, opfunc in convert[root]:
+        if opname not in roots:
+            opfunc.__name__ = opname
+            opfunc.__doc__ = getattr(int, opname).__doc__
+            setattr(cls, opname, opfunc)
+    return cls
+
+def cmp_to_key(mycmp):
+    'Convert a cmp= function into a key= function'
+    class K(object):
+        def __init__(self, obj, *args):
+            self.obj = obj
+        def __lt__(self, other):
+            return mycmp(self.obj, other.obj) < 0
+        def __gt__(self, other):
+            return mycmp(self.obj, other.obj) > 0
+        def __eq__(self, other):
+            return mycmp(self.obj, other.obj) == 0
+        def __le__(self, other):
+            return mycmp(self.obj, other.obj) <= 0
+        def __ge__(self, other):
+            return mycmp(self.obj, other.obj) >= 0
+        def __ne__(self, other):
+            return mycmp(self.obj, other.obj) != 0
+        def __hash__(self):
+            raise TypeError('hash not implemented')
+    return K

Modified: python/branches/py3k/Lib/pstats.py
==============================================================================
--- python/branches/py3k/Lib/pstats.py	(original)
+++ python/branches/py3k/Lib/pstats.py	Mon Apr  5 20:56:31 2010
@@ -37,6 +37,7 @@
 import time
 import marshal
 import re
+from functools import cmp_to_key
 
 __all__ = ["Stats"]
 
@@ -226,7 +227,7 @@
             stats_list.append((cc, nc, tt, ct) + func +
                               (func_std_string(func), func))
 
-        stats_list.sort(key=CmpToKey(TupleComp(sort_tuple).compare))
+        stats_list.sort(key=cmp_to_key(TupleComp(sort_tuple).compare))
 
         self.fcn_list = fcn_list = []
         for tuple in stats_list:
@@ -458,15 +459,6 @@
                 return direction
         return 0
 
-def CmpToKey(mycmp):
-    'Convert a cmp= function into a key= function'
-    class K(object):
-        def __init__(self, obj):
-            self.obj = obj
-        def __lt__(self, other):
-            return mycmp(self.obj, other.obj) == -1
-    return K
-
 
 #**************************************************************************
 # func_name is a triple (file:string, line:int, name:string)

Modified: python/branches/py3k/Lib/test/test_functools.py
==============================================================================
--- python/branches/py3k/Lib/test/test_functools.py	(original)
+++ python/branches/py3k/Lib/test/test_functools.py	Mon Apr  5 20:56:31 2010
@@ -364,7 +364,89 @@
         d = {"one": 1, "two": 2, "three": 3}
         self.assertEqual(self.func(add, d), "".join(d.keys()))
 
-
+class TestCmpToKey(unittest.TestCase):
+    def test_cmp_to_key(self):
+        def mycmp(x, y):
+            return y - x
+        self.assertEqual(sorted(range(5), key=functools.cmp_to_key(mycmp)),
+                         [4, 3, 2, 1, 0])
+
+    def test_hash(self):
+        def mycmp(x, y):
+            return y - x
+        key = functools.cmp_to_key(mycmp)
+        k = key(10)
+        self.assertRaises(TypeError, hash(k))
+
+class TestTotalOrdering(unittest.TestCase):
+
+    def test_total_ordering_lt(self):
+        @functools.total_ordering
+        class A:
+            def __init__(self, value):
+                self.value = value
+            def __lt__(self, other):
+                return self.value < other.value
+        self.assert_(A(1) < A(2))
+        self.assert_(A(2) > A(1))
+        self.assert_(A(1) <= A(2))
+        self.assert_(A(2) >= A(1))
+        self.assert_(A(2) <= A(2))
+        self.assert_(A(2) >= A(2))
+
+    def test_total_ordering_le(self):
+        @functools.total_ordering
+        class A:
+            def __init__(self, value):
+                self.value = value
+            def __le__(self, other):
+                return self.value <= other.value
+        self.assert_(A(1) < A(2))
+        self.assert_(A(2) > A(1))
+        self.assert_(A(1) <= A(2))
+        self.assert_(A(2) >= A(1))
+        self.assert_(A(2) <= A(2))
+        self.assert_(A(2) >= A(2))
+
+    def test_total_ordering_gt(self):
+        @functools.total_ordering
+        class A:
+            def __init__(self, value):
+                self.value = value
+            def __gt__(self, other):
+                return self.value > other.value
+        self.assert_(A(1) < A(2))
+        self.assert_(A(2) > A(1))
+        self.assert_(A(1) <= A(2))
+        self.assert_(A(2) >= A(1))
+        self.assert_(A(2) <= A(2))
+        self.assert_(A(2) >= A(2))
+
+    def test_total_ordering_ge(self):
+        @functools.total_ordering
+        class A:
+            def __init__(self, value):
+                self.value = value
+            def __ge__(self, other):
+                return self.value >= other.value
+        self.assert_(A(1) < A(2))
+        self.assert_(A(2) > A(1))
+        self.assert_(A(1) <= A(2))
+        self.assert_(A(2) >= A(1))
+        self.assert_(A(2) <= A(2))
+        self.assert_(A(2) >= A(2))
+
+    def test_total_ordering_no_overwrite(self):
+        # new methods should not overwrite existing
+        @functools.total_ordering
+        class A(int):
+            raise Exception()
+        self.assert_(A(1) < A(2))
+        self.assert_(A(2) > A(1))
+        self.assert_(A(1) <= A(2))
+        self.assert_(A(2) >= A(1))
+        self.assert_(A(2) <= A(2))
+        self.assert_(A(2) >= A(2))
 
 
 def test_main(verbose=None):

Modified: python/branches/py3k/Lib/unittest/loader.py
==============================================================================
--- python/branches/py3k/Lib/unittest/loader.py	(original)
+++ python/branches/py3k/Lib/unittest/loader.py	Mon Apr  5 20:56:31 2010
@@ -5,6 +5,7 @@
 import sys
 import traceback
 import types
+import functools
 
 from fnmatch import fnmatch
 
@@ -141,7 +142,7 @@
         testFnNames = testFnNames = list(filter(isTestMethod,
                                                 dir(testCaseClass)))
         if self.sortTestMethodsUsing:
-            testFnNames.sort(key=util.CmpToKey(self.sortTestMethodsUsing))
+            testFnNames.sort(key=functools.cmp_to_key(self.sortTestMethodsUsing))
         return testFnNames
 
     def discover(self, start_dir, pattern='test*.py', top_level_dir=None):

Modified: python/branches/py3k/Lib/unittest/util.py
==============================================================================
--- python/branches/py3k/Lib/unittest/util.py	(original)
+++ python/branches/py3k/Lib/unittest/util.py	Mon Apr  5 20:56:31 2010
@@ -70,15 +70,6 @@
     # anything left in actual is unexpected
     return missing, actual
 
-def CmpToKey(mycmp):
-    'Convert a cmp= function into a key= function'
-    class K(object):
-        def __init__(self, obj, *args):
-            self.obj = obj
-        def __lt__(self, other):
-            return mycmp(self.obj, other.obj) == -1
-    return K
-
 def three_way_cmp(x, y):
     """Return -1 if x < y, 0 if x == y and 1 if x > y"""
     return (x > y) - (x < y)

Modified: python/branches/py3k/Misc/NEWS
==============================================================================
--- python/branches/py3k/Misc/NEWS	(original)
+++ python/branches/py3k/Misc/NEWS	Mon Apr  5 20:56:31 2010
@@ -303,6 +303,8 @@
 Library
 -------
 
+- Add functools.total_ordering() and functools.cmp_to_key().
+
 - Issue #8257: The Decimal construct now accepts a float instance
   directly, converting that float to a Decimal of equal value:
 


More information about the Python-checkins mailing list