Commits

Jonathan Eunice committed 0e95dfc

Added OptionsClass base class.

Comments (0)

Files changed (8)

 Change Log
 ==========
 
+1.1 (October 29, 2103)
+''''''''''''''''''''''''
+
+  * Added ``OptionsClass`` base class. If client classes inherit
+    from this, they automatically get ` set()`` and ``settings()``
+    methods.
+
 1.0.7 (October 25, 2103)
 ''''''''''''''''''''''''
 
 1.0.4 - 1.0.6 (October 24, 2013)
 ''''''''''''''''''''''''''''''''
   
-  * When bad option names are defined ("bad" here meaning "conflicts with
-    names already chosen for pre-existing methods"), a ``BadOptionName``
+* When bad option names are defined ("bad" here meaning "conflicts with
+        names already chosen for pre-existing methods"), a ``BadOptionName``
     exception will be raised.
   * Tweaked docs, adding comparison chart.
 
 Change Log
 ==========
 
+1.1 (October 29, 2103)
+''''''''''''''''''''''''
+
+  * Added ``OptionsClass`` base class. If client classes inherit
+    from this, they automatically get ` set()`` and ``settings()``
+    methods.
+
 1.0.7 (October 25, 2103)
 ''''''''''''''''''''''''
 
 1.0.4 - 1.0.6 (October 24, 2013)
 ''''''''''''''''''''''''''''''''
   
-  * When bad option names are defined ("bad" here meaning "conflicts with
-    names already chosen for pre-existing methods"), a ``BadOptionName``
+* When bad option names are defined ("bad" here meaning "conflicts with
+        names already chosen for pre-existing methods"), a ``BadOptionName``
     exception will be raised.
   * Tweaked docs, adding comparison chart.
 
 # built documents.
 #
 # The short X.Y version.
-version = '1.0'
+version = '1.1'
 # The full version, including alpha/beta/rc tags.
-release = '1.0.8'
+release = '1.1.0'
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
 needs one line of code to decide whether the override is, in fact, in effect.
 Suddenly dealing with parameters starts to be a full-time job, as every possible
 setting has to be managed in every method. That's neither elegant nor scalable.
+Pretty soon we're in "just one more wafer-thin mint..." territory.
 
-Things get even worse if we want to change the default value for all shapes in the
-class. We have to rework every method that uses values, the ``__init__`` method,
-*et cetera*. We've entered "just one more wafer-thin mint..." territory.
+But with ``options``, it's easy. No matter how many configuration variables there
+are to be managed, each method needs just one line of code to manage them::
 
-But with ``options``, it's easy::
+    opts = self.options.push(kwargs)
+
+Changing things works simply and logically::
 
     Shape.options.set(color='blue')
     one.draw()
+    one.options.set(color='red')
     one.draw(height=100)
     one.draw(height=44, color='yellow')
 
 yields::
 
     color='blue', width=10, name='one', height=10
-    color='blue', width=10, name='one', height=100
+    color='red', width=10, name='one', height=100
     color='yellow', width=10, name='one', height=44
 
-In one line, we reset the default for all ``Shape`` objects. (In typical usage
-we'd also define ``Shape.set()`` to transparently forward
-to ``Shape.options.set()`` for an even simpler resulting API.)
+In one line, we reset the default color for all ``Shape`` objects. That's
+visible in the next call to ``one.draw()``. Then we set the instance color
+of ``one`` (all other ``Shape`` instances remain blue). Finally, We call
+one with a temprary override of the color.
+
+A common pattern makes this even easier::
+
+    class Shape(OptionsClass):
+        ...
+
+The ``OptionsClass`` base class will provide a ``set()`` method so that you
+don't need ``Shape.options.set()``. ``Shape.set()`` does the same thing,
+resulting in an even simpler API. The ``set()`` method is a "combomethod" that
+will take either a class or an instance and "do the right thing."
+``OptionsClass`` also provides a ``settings()`` method to easily handle
+transient ``with`` contexts (more on this in a minute).
 
 The more options and settings a class has, the more unwieldy the
 class and instance variable approach becomes, and the more desirable
     if is_tall(one):    # nope, not here!
         ...
 
-Full disclosure: Doing temporary settings took slightly more class
-setup code than is shown above. Four lines of code, to be precise.
+.. note:: If you don't wish to inherit from ``OptionsClass``, you can
+    manually add ``set()`` and ``settings()`` methods to your own class.
+    See the ``OptionsClass`` source code for details.
 
 As one final feature, consider "magical" parameters. Add the following
 code::

options/__init__.py

 from options.core import (Unset, Prohibited, Transient, attrs,
-    Options, OptionsChain, OptionsContext, BadOptionName)
+    Options, OptionsChain, OptionsClass, OptionsContext, BadOptionName)
 from options.version import __version__
 from options.configparser import ConfigParser
 from options.nulltype import NullType
 from options.funclike import *
+from options.combomethod import combomethod
 
 
 # Define sentinel objects
     return ', '.join(["{0}={1}".format(k, repr(m[k])) for k in keys])
 
 
+class OptionsClass(object):
+
+    """
+    Class from which client classes can inherit. It will add ``set`` and
+    ``settings`` methods.
+    """
+
+    @combomethod
+    def set(receiver, **kwargs):
+        """
+        Change the receiver's settings to those defined in the kwargs.
+        An update-like function. This uplevels calls that would look
+        like ``Class.options.set(...)`` to the simpler ``Class.set(...)``.
+        Works on either class or instance receivers. Requires that one
+        uses the instance variable ``options`` to store persistent
+        configuration data.
+        """
+        receiver.options.set(**kwargs)
+
+    def settings(self, **kwargs):
+        """
+        Open a context manager for a `with` statement. Temporarily change settings
+        for the duration of the with.
+        """
+        return OptionsContext(self, kwargs)
+
 class Options(orderedstuf):
 
     """

options/version.py

-__version__ = '1.0.8'
+__version__ = '1.1.0'
 
     assert not is_tall(one)
 
+def test_docs_example_with_OptionsClass():
+
+    def dicty(s):
+        """
+        Test helper. Helps test dict equivalence. Given different versions of Python,
+        stringified dicts may have different item order. This gets us back to real
+        dicts, which have more logical equality operations.
+        """
+        d = eval('dict({0})'.format(s))
+        if '_magic' in d:
+            del d['_magic']
+        return d
+
+    class Shape(OptionsClass):
+
+        options = Options(
+            name   = None,
+            color  = 'white',
+            height = 10,
+            width  = 10,
+        )
+
+        def __init__(self, **kwargs):
+            self.options = Shape.options.push(kwargs)
+
+        def draw(self, **kwargs):
+            opts = self.options.push(kwargs)
+            # in this case, return actual, simplified dict, not string (docs shows string return)
+            d = dict(opts)
+            if '_magic' in d:
+                del d['_magic']
+            return d
+
+    one = Shape(name='one')
+    assert one.draw() == dicty("color='white', width=10, name='one', height=10")
+    assert one.draw(color='red') == dicty("color='red', width=10, name='one', height=10")
+    assert one.draw(color='green', width=22) == dicty("color='green', width=22, name='one', height=10")
+
+
+    Shape.set(color='blue')
+    assert one.draw() == dicty("color='blue', width=10, name='one', height=10")
+    assert one.draw(height=100) == dicty("color='blue', width=10, name='one', height=100")
+    assert one.draw(height=44, color='yellow') == dicty("color='yellow', width=10, name='one', height=44")
+
+    one.set(color='blue')
+    Shape.set(color='pink')
+    assert one.draw() == dicty("color='blue', width=10, name='one', height=10")
+    assert one.draw(height=100) == dicty("color='blue', width=10, name='one', height=100")
+    assert one.draw(height=44, color='yellow') == dicty("color='yellow', width=10, name='one', height=44")
+
+    def is_tall(self, **kwargs):
+        opts = self.options.push(kwargs)
+        return opts.height > 100
+
+    assert not is_tall(one)
+
+    with one.settings(height=200, color='purple'):
+        assert one.draw() == dicty("color='purple', width=10, name='one', height=200")
+        assert is_tall(one)
+
+    assert not is_tall(one)
+
 def test_no_aliases():
     # o works just fine
     o = Options(color='red', shape='box')