Commits

Jonathan Eunice  committed 1603e87

Minor cleanups. Much-improved PEP8 conformance. Improved handling of Transient. Added it to __all__ (it was accidentally missing) and added setdefault method (for OptionChains only).

  • Participants
  • Parent commits a1f65aa

Comments (0)

Files changed (8)

             if 'suffix' not in used:
                 self.options.suffix = self.options.prefix
 
+Related Work
+============
+
+A huge amount of work, both in Python and beyond, has gone into
+the effective management of configuration information. 
+
+ * Program defaults. Values pre-established by developers, often
+   as ``ALL_UPPERCASE_IDENTIFIERS`` or as keyword default to 
+   functions.
+
+ * Configuration file format parsers/formatters.
+   Huge amounts of the INI, JSON, XML, and YAML specifications and
+   toolchains, for example, are configuration-related. 
+   There are many. `anyconfig <https://pypi.python.org/pypi/anyconfig>`_
+   is perhaps of interest for its flexibility.
+   You could
+   probably lump into this group binary data marshalling schemes
+   such as ``pickle``.
+
+ * Command-line argument parsers. These are all about taking configuration
+   information from the command line. 
+   `argh <https://pypi.python.org/pypi/argh>`_ is one I particularly
+   like for its simple, declarative nature.
+   (`aaargh <https://pypi.python.org/pypi/aaargh>`_ is similar.)
+
+ * System and environment introspection. The best known of these
+   would be ``sys.argv`` and
+   ``s.environ`` to get command line arguments and the values
+   of operating system environment variables (especially when running
+   on Unixy platforms). But any code that asks "Where am I running?"
+   or "What is my IP address?" or otherwise inspects its current
+   execution environment and configures itself accordingly
+   is doing a form of configuration discovery.
+
+ * Attribute-accessible dictionary objects. It is incredibly easy to
+   create simple versions of this idea in Python--and rather tricky
+   to create robust, full-featured versions. Caveat emptor.  `stuf
+   <https://pypi.python.org/pypi/stuf>`_ and `treedict
+   <https://pypi.python.org/pypi/treedict>`_ are cream-of-the-crop
+   implementations of this idea. I have not tried `confetti
+   <https://pypi.python.org/pypi/confetti>`_ or 
+   `Yaco <https://pypi.python.org/pypi/Yaco>`_, but they look like
+   variations on the same theme.
+
+ * The portion of Web frameworks concerned with getting and setting 
+   cookies, URL query and hash attributes, form variables, and/or 
+   HTML5 local stroage. Not that 
+   these are pariticularly secure, scalable, or robust sources...but they're 
+   important configuration information nonetheless.
+
+ * While slighly afield, database interface modules are often used for
+   querying configuration information from SQL or NoSQL databases.
+
+ * Some object metaprogramming systems. That's a mouthful, right?
+   Well some modules implement metaclasses that change the basic
+   behavior of objects. `value <https://pypi.python.org/pypi/value>`_
+   for example provides very common-sense treatment of object
+   instantiation with out all the Javaesque 
+   ``self.x = x; self.y = y; self.z = z`` repetition.
+   ``options`` similarly redesigns how parameters should be passed
+   and object values stored.
+
+ * Combomatics. Many configuration-related modules combine two
+   or more of these approaches. E.g. 
+   `yconf <https://pypi.python.org/pypi/yconf>`_
+   combines YAML config file parsing with ``argparse`` command
+   line parsing. In the future, ``options`` will also follow
+   this path. There's no need to take programmer time and 
+   attention for several different low-level
+   configuration-related tasks.
+
+ * Dependency injection frameworks are all about providing configuration
+   information from outside code modules. They tend to be rather
+   abstract and have a high "activation energy," but the more complex
+   and composed-of-many-different-components your system is, the
+   more valuable the "DI pattern" becomes.
+
+ * Other things. 
+   `conflib <https://pypi.python.org/pypi/conflib>`_, uses dictionary
+   updates to stack
+   default, global, and 
+   local settings; it also provides a measure of validation.
+
+
+This diversity, while occasionally frustrating, makes some sense.
+Configruation data, after all, is just "state," and "managing state"
+is pretty much what all computing is about.
+Pretty much every program has to do it. That so many use so
+many different, home-grown ways is why there's such a good opportunity.
+
+`Flask's documentation <http://flask.pocoo.org/docs/config/#configuring-from-files>`_ is a real-world example of how "spread everywhere" this can be,
+with some data coming from the program, some from files, some from
+environment variables, some from Web-JSON, etc.
+
 Notes
 =====
 
 
     python3.3 -m easy_install options
     
-(You may need to prefix these with "sudo " to authorize installation.)
+(You may need to prefix these with "sudo " to authorize installation.)

File options/__init__.py

 from options.core import *
 
-__all__ = 'Unset Prohibited attrs Options OptionsChain OptionsContext'.split()
+__all__ = 'Unset Prohibited Transient attrs Options OptionsChain OptionsContext'.split()

File options/core.py

 from options.configparser import ConfigParser
 from options.nulltype import NullType
 from options.funclike import *
-    
+
 Unset      = NullType('Unset')
 Prohibited = NullType('Prohibited')
 Transient  = NullType('Transient')
 
+
 def attrs(m, first=[], underscores=False):
     """
     Given a mapping m, return a string listing its values in a
             continue
         if k not in first:
             keys.append(k)
-    return ', '.join([ "{0}={1}".format(k, repr(m[k])) for k in keys ])
+    return ', '.join(["{0}={1}".format(k, repr(m[k])) for k in keys])
+
 
 class Options(orderedstuf):
+
     """
     Options handler.
     """
-    
+
     def __init__(self, *args, **kwargs):
         orderedstuf.__init__(self, *args)
-        for k,v in kwargs.items():
+        for k, v in kwargs.items():
             self[k] = v
         self._magic = {}
-        
+
         # The explicit for loop setting values from kwargs should not be necessary.
         # orderedstuf.__init__(self, *args, **kwargs) should do the trick. And it
         # does, on most platforms. But on PyPy, stuf(a=sys.stdout) fails in much
         # the same way used to on Python 3. Until stuf/orderedstuf is fixed for
         # PyPy, this workaround fixes the issue for Options.
-    
+
     def __repr__(self):
         return "{0}({1})".format(self.__class__.__name__, attrs(self))
-    
+
     def set(self, **kwargs):
         # To set values at the base options level, create a temporary next level,
         # which will have the magical option interpretation. Then copy the resulting
         # values here. Do in original ordering.
         oc = OptionsChain(self, kwargs)
         for key in self.keys():
-            self[key] = oc[key] 
-    
+            self[key] = oc[key]
+
     def push(self, kwargs):
         """
         Create the next layer down. Intended for instances to call during
         
         """
         return OptionsChain(self, kwargs)
-    
+
         # NB the implicit addflat() call is gone
-    
-    #def __iter__(self):
+
+    # def __iter__(self):
     #    for k in super(Options, self).keys():
     #        if not k.startswith('_'):
     #            yield k
         """
         Return items of self, but none that are 'internal' (ie start with underscore _)
         """
-        return [ (k,v) for (k,v) in super(Options, self).items() if not k.startswith('_') ]
-  
+        return [(k, v) for (k, v) in super(Options, self).items() if not k.startswith('_')]
+
     def add(self, **kwargs):
         """
         Create the next layer down. Like ``push()``, but accepts full kwargs
         for key, value in kwargs.items():
             child[key] = value
         return child
-    
+
     def magic(self, **kwargs):
         """
         Set some options as having 'magical' update properties. In a sense, this
         self.setdefault('_magic', {})
         for k, v in kwargs.items():
             self._magic[k] = real_func(v)
-    
+
     def magical(self, key):
         """
         Instance based decorator, specifying a function in the using module
         as a magical function. Note that the magical methods will be called
         with a self of None. 
         """
-            
         def my_decorator(func):
             self._magic[key] = func
             return func
-        
+
         return my_decorator
-    
+
     def write(self, filepath):
         """
         Save configuration values to the given filepath.
         additional = dict(zip(keys, args))
         self.update(additional)
         keys_used = list(additional.keys())
-        return keys_used 
+        return keys_used
 
     def add(self, **kwargs):
         """
         for key, value in kwargs.items():
             child[key] = value
         return child
-    
+
     def set(self, **kwargs):
         newopts = self._process(self, kwargs)
         for k, v in newopts.items():
             else:
                 self.maps[0][k] = v
                 
+
+    def setdefault(self, key, default=None):
+        """
+        If key is in the dictionary, return its value. If not,
+        insert key with a value of default and return default.
+        default defaults to None. If the value is Prohibited,
+        no change will be allowed, and a KeyError will be raised.
+        If the value is Transient, it will be as if the value
+        were never there, and the default will be set.
+        """
+        try:
+            value = self[key]
+            if value is Prohibited:
+                raise KeyError("changes to '{0}' are prohibited".format(key))
+            elif value is Transient:
+                self[key] = default
+                return default
+            return value
+        except KeyError as e:
+            raise e
+    
+    
     def copy(self):
         """
         Return a copy of the self. That means a copy of just the top layer,
         for k, v in self.items():
             new[k] = v
         return new
-  
+
     # TBD Need to examine all the places we need to check for Prohibited
     # when setting values - eg, what about in Options
-    
+
     # Do we want to define a more specific exception for Prohibited?
 
     # TBD when using Prohibited values, __getattr__ and __getitem_ should raise
     # careful of recursions). Current draft doesn't work - infinite recursion
     
 class OptionsContext(object):
+
     """
     Context manager so that modules that use Options can easily implement
     a `with x.settings(...):` capability. In x's class:
         When `with x.method(*args, **kwargs)` is called, it creates an OptionsContext
         passing in its **kwargs. 
         """
-        self.caller = caller        
+        self.caller = caller
         if 'opts' in kwargs:
             newopts = OptionsChain(caller.options, kwargs['opts'])
             newopts.maps.insert(0, caller._process(newopts, kwargs))
         Called when the `with` is about to be 'entered'. Whatever this returns
         will be the value of `x` if the `as x` construction is used. Not generally
         needed for option setting, but might be needed in a subclass.
-        """            
+        """
         return self.caller
-    
+
     def __exit__(self, exc_type, exc_value, traceback):
         """
         Called when leaving the `with`. Reset caller's options to what they were
     # might be to provide per-thread options, with _get_options() looking
     # options up in a tid-indexed hash, and set() operations creating a copy
     # (copy-on-write, per thread, essentially).
-    
+
     # with module quoter have found some interesting use cases like abbreviation
     # args (MultiSetter), and have further explored how subclasses will integrate
     # with the delegation-based options. Have discovered that some subclasses will
     # want flat args, prohibit superclass args. Also, some args could potentially
     # allow setting at instantiation time but not call/use time.
-    
+
     # Would it make sense to support magicalized class setting -- for subclasses?
     # even if it would, how to accomplish neatly? probably would require a
-    # property like object that lets you assign the magic along with the initial value
-    
+    # property like object that lets you assign the magic along with the
+    # initial value
+
     # next step: integrate setter from testprop.py
     # consider adding _for key pointing to object that options are for
     # also providing access to whole dict for arguments like chars
     # and clean up HTMLQuoter using the improved functions & access
-    
+
     # quoter might provide the usecase for a getter, too - in that suffix is == prefix
     # if suffix itself not given
-    
-    # instead of Magic() or Setter() maybe Property() or Prop()
+
+    # instead of Magic() or Setter() maybe Property() or Prop()

File options/funclike.py

 
 function_type = type(lambda: True)   # define our own because not def'd in py26
 
+
 def is_function(obj):
     """
     Determines: Is obj a function?
     """
     return isinstance(obj, function_type)
-    
+
+
 def real_func(flike):
     """
     Given a function-like object (function or staticmethod), return the real function.
     """
     if is_function(flike):
         return flike
-    elif hasattr(flike, 'im_func'):     # Not clear this still useful; was for methods < py26 
+    # Not clear this still useful; was for methods < py26
+    elif hasattr(flike, 'im_func'):
         return flike.im_func
     elif hasattr(flike, '__func__'):    # method (static or otherwise) >= p26
         return flike.__func__
         return flike.__get__(True)
     else:
         raise ValueError("doesn't seem to be a function-like object")
-    
+
     # Introspection: Isn't it fun?! It's valuable...but changing in the underlying
     # data model or implementation => oy vey!
-    
+
+
 def func_code(flike):
     """
     Given a function like object (function, static method, etc.), return its
         return flike.func_code
     elif hasattr(flike, '__code__'):   # Python 3
         return flike.__code__
-    else:       
+    else:
         raise ValueError("don't know where to find function's code")

File options/nulltype.py

 
 _PY3 = sys.version_info[0] == 3
 
+
 class NullType(object):
+
     """
     A 'null' type different from, but parallel to, None. Core function
     is representing emptyness in a way that doesn't overload None.
     enforcement to maintain their singleton-ness. This is a problem
     roughly 0% of the time.
     """
+
     def __init__(self, name=None):
         self.name = name
-        
+
     def __repr__(self):
         if self.name is not None:
             return self.name
         else:
             return repr(self)
 
-    
     if _PY3:
         def __bool__(self):
             """I am always False."""
             return False
-    else: # PY2
+    else:  # PY2
         def __nonzero__(self):
             """I am always False."""
             return False
 #! /usr/bin/env python
 
 from setuptools import setup, find_packages
-from decimal import Decimal
-import re
 
 def linelist(text):
     """
 
 setup(
     name='options',
-    version=0.45,
+    version='0.5',
     author='Jonathan Eunice',
     author_email='jonathan.eunice@gmail.com',
     description='Container for flexible class, instance, and function call options',
     long_description=open('README.rst').read(),
     url='https://bitbucket.org/jeunice/options',
     packages=['options'],
-    install_requires=['stuf>=0.9.10', 'six'],
+    install_requires=['stuf>=0.9.12', 'six'],
     tests_require = ['tox', 'pytest'],
-    zip_safe = True,
+    zip_safe = False,
     keywords='options config configuration parameters arguments',
     classifiers=linelist("""
         Development Status :: 4 - Beta

File test/test.py

     assert o2.slick == 'slack'
     assert o2.blik  == 'rubber'
     assert o2.this  == 4
+    
+    # now make a copy - check data is the same
     o3 = o2.copy()
     assert o3 is not o2
     assert o3.nick == 'nack'
     assert o3.slick == 'slack'
     assert o3.blik  == 'rubber'
     assert o3.this  == 4
+    
+    # make sure copy is not using sam
     o2.slick = 999
     assert o2.slick == 999
     assert o3.slick == 'slack'
- 
+
+#def test_child():
+#    options = Options(
+#            this=1,
+#            slick='slack',
+#            nick='nack',
+#            blik=99,
+#            man='man'
+#        )
+#    o1 = options.child()
+#    assert o1 is not options
+#    assert o1 == options
+#    
+#    o2 = options.push(dict(this=4, blik='rubber'))
+#    assert o2.nick == 'nack'
+#    assert o2.slick == 'slack'
+#    assert o2.blik  == 'rubber'
+#    assert o2.this  == 4
+#    
+#    # now make a copy - check data is the same
+#    o3 = o2.child()
+#    assert o3 is not o2
+#    assert o3.nick == 'nack'
+#    assert o3.slick == 'slack'
+#    assert o3.blik  == 'rubber'
+#    assert o3.this  == 4
+#    
+#    # make sure copy is not using sam
+#    o2.slick = 999
+#    assert o2.slick == 999
+#    assert o3.slick == 'slack'
+#    
+#    # but make sure some things shine through
+#    options.set(man='MAN')
+#    assert o2.man == 'MAN'
+#    assert o3.man == 'MAN'
+
+
 #@pytest.mark.skipif('True')  #under construction
 #def test_write(tmpdir):
 #    o = Options(
     six
     pytest
 commands=py.test {posargs}
+
+[testenv:py34]
+basepython=python3.4
+changedir=test
+deps=
+    six
+    pytest
+commands=py.test {posargs}