[Zope-Coders] Environment variables

Tres Seaver tseaver@zope.com
29 Apr 2002 11:44:23 -0400


--=-Kl94KR0j8bN0qEF4CB8t
Content-Type: text/plain
Content-Transfer-Encoding: 7bit

On Mon, 2002-04-29 at 08:56, Matt Behrens wrote:
> Next brief topic up for discussion: instance.conf is going to at
> least support environment variable setting, a la
> 
> [Environment]
> STUPID_LOG_FILE=/foo/bar/baz
> STUPID_LOG_SEVERITY=-200
> 
> However, I'd rather go down the path of eliminating environment
> variables for specifying future behavior entirely.
> 
> Specifically, I'd like to be able to take an instance of my
> runtime-overridable ConfigParser that is almost done being written
> in and make it available globally somehow, so we could at least
> eliminate future os.environ use.  Since this would (I assume) involve
> putting this into the globally available namespace, and probably
> has lots of ramifications I don't understand, I'd like an experienced
> commentary on how best to do this.

I think the Right Thing (TM) is to add a new module (perhaps inside
the 'App' package) called 'Options' or 'Configuration'.  It would be the
central repository for knowledge of configuration values, whether set
from the 'instance.conf' file or from command-line or environment
variables.  I am attaching a file which illustrates this strategy.

-- 
===============================================================
Tres Seaver                                tseaver@zope.com
Zope Corporation      "Zope Dealers"       http://www.zope.com

--=-Kl94KR0j8bN0qEF4CB8t
Content-Disposition: attachment; filename=ZopeConfiguration.py
Content-Transfer-Encoding: quoted-printable
Content-Type: text/x-python; name=ZopeConfiguration.py; charset=ISO-8859-1

"""
    Manage Zope configuration options, which may be set from=20

    Functions:

      - getConfigurationValue

      - getConfigurationSource

      - setConfigurationValue

"""
BASE_DIR =3D '/home/zope'
CONFIG_FILE_NAME =3D 'etc/zope.conf'

#
#   Configuration sources
#
DEFAULT      =3D 0  # Defined in software.
CONFIG_FILE  =3D 1  # Defined in configuration file.
ENVIRONMENT  =3D 2  # Defined in environment variable
COMMAND_LINE =3D 3  # Defined by command-line option.

#
#   Registry for options
#
_all_options =3D {}
_environment_index =3D {}
_long_options_index =3D {}
_short_options_index =3D {}

def _clear():
    global _all_options
    global _environment_index
    global _long_options_index
    global _short_options_index
    _all_options =3D {}
    _environment_index =3D {}
    _long_options_index =3D {}
    _short_options_index =3D {}

def registerConfigurationOption( config_file_key
                               , default_value=3DNone
                               , is_switch=3D0
                               , description=3D''
                               , environment_key=3DNone
                               , short_option=3DNone
                               , long_option=3DNone,=20
                               ):
    """
        Register a possible configuration option, including its
        default value and the optional channels for setting it.
    """
    global _all_options
    global _environment_index
    global _long_options_index
    global _short_options_index

    option =3D ConfigurationOption( config_file_key
                                , default_value
                                , is_switch
                                , description
                                , environment_key
                                , long_option
                                , short_option
                                )

    _all_options[ config_file_key ] =3D option

    if environment_key:
        _environment_index[ environment_key ] =3D option

    if long_option:
        _long_options_index[ long_option ] =3D option

    if short_option:
        _short_options_index[ short_option ] =3D option

#
#   Configuration option specification
#
class ConfigurationOption:

    def __init__( self
                , config_file_key
                , default_value
                , is_switch=3D0
                , description=3DNone
                , environment_key=3DNone
                , long_option=3DNone=20
                , short_option=3DNone
                ):
        ""
        self._source =3D DEFAULT
        self._config_file_key =3D config_file_key
        self._value =3D default_value
        self._is_switch =3D is_switch
        self._description =3D description
        self._environment_key =3D environment_key
        self._short_option =3D short_option
        self._long_option =3D long_option

    def getConfigFileKey( self ):
        """
            Return the key used to define this value in a configuration
            file.
        """
        return self._config_file_key

    def isSwitch( self ):
        """
            Is this option a switch?
        """
        return self._is_switch

    def getEnvironmentKey( self ):
        """
            Return the key used to define this value as an environment
            variable (defaults to None, meaning the value cannot be
            set via the environment).
        """
        return self._environment_key

    def getShortOption( self ):
        """
            Return the key used to define this value as a "short"
            command-line option (defaults to None, meaning the value
            cannot be set via the command-line).
        """
        return self._short_option

    def getLongOption( self ):
        """
            Return the key used to define this value as a "long"
            command-line option (defaults to None, meaning the value
            cannot be set via the command-line).
        """
        return self._long_option

    def getDescription( self ):
        """
            Return the description of this option.
        """
        return self._description

    def getValue( self ):
        """
            Return the value of this option.
        """
        return self._value

    def getSource( self ):
        """
            Return the source for the value assigned to this option.
        """
        return self._source

    def setValue( self, value, source ):
        """
            Update the value assigned to this option, and its source.

            'source' should be one of the constants declared above.
        """
        self._value =3D value
        self._source =3D source

#
#   Configuration API
#
def getConfigurationInfo( key ):
    """
        Return the configuration option corresponding to 'key', or
        None if no option has been registered for that key.

        TODO:  Search other indexes, too.
    """
    indexes =3D ( _all_options
              , _environment_index
              , _long_options_index
              , _short_options_index
              )

    for index in indexes:
        info =3D index.get( key )
        if info is not None:
            return info
    return None

def getConfigurationValue( key, default=3DNone ):
    """
        Return the configuration value corresponding to 'key', or
       'default' if no value exists.
    """
    option =3D getConfigurationInfo( key )
    if option:
        return option.getValue()
    return default

def getConfigurationSource( key ):
    """
        Return the source of the configuration value corresponding to
       'key'.  Source should be one of the
       known constants defined above, or None if no value exists=20
    """
    option =3D getConfigurationInfo( key )
    if option:
        return option.getSource()
    return None

def parseConfigFile( fully_qualified_path=3DNone ):
    """
        Parse the config file (default is '/home/zope/etc/zope.conf').
    """
    if fully_qualified_path is None:
        fully_qualified_path =3D '%s/%s' % ( BASE_DIR, CONFIG_FILE_NAME )

    g =3D { '__builtins__'    : {}
        , 'None'            : None
        }

    l =3D {}

    execfile( fully_qualified_path, g, l )

    for k, v in l.items():
        option =3D getConfigurationInfo( k )
        if option:
            option.setValue( v, CONFIG_FILE )
        else:
            pass    # TODO:  log warning

def parseEnvironment( env ):
    """
        Extract relevant environment variables.
    """
    for key, option in _environment_index.items():
        value =3D env.get( key )
        if value is None:
            continue
        if option.isSwitch():
            value =3D 1
        option.setValue( value, ENVIRONMENT )

def parseCommandLine( argv ):
    """
        Extract relevant command-line options, returning remaining
        arguments.

        Raise getopt.GetoptError if something doesn't match.
    """
    import getopt

    shorts =3D []
    for short, option in _short_options_index.items():
        if not option.isSwitch():
            short =3D short + ':'
        shorts.append( short )

    longs =3D []
    for long, option in _long_options_index.items():
        if not option.isSwitch():
            long =3D long + '=3D'
        longs.append( long )

    options, arguments =3D getopt.getopt( argv, ''.join( shorts ), longs )

    for k, v in options:
        if k.startswith( '--' ):
            option =3D _long_options_index[ k[2:] ]
        else:
            option =3D _short_options_index[ k[1:] ]
        if option.isSwitch():
            option.setValue( 1, COMMAND_LINE )
        else:
            option.setValue( v, COMMAND_LINE )

    return arguments

def parseOptions():
    """
        Do the default setup
    """
    import sys, os
    parseConfigFile()
    parseEnvironment( os.environ )
    parseCommandLine( sys.argv )


if __name__ =3D=3D '__main__':

    import unittest

    class Tests( unittest.TestCase ):

        def setUp( self ):

            self._old_BASE_DIR =3D BASE_DIR
            self._old_CONFIG_FILE_NAME =3D CONFIG_FILE_NAME

            registerConfigurationOption( 'foo'
                                       , 'Foo_default'
                                       , 0
                                       , 'Foo description.'
                                       , 'FOO_ENV'
                                       , 'f'
                                       , 'foo'
                                       )

            registerConfigurationOption( 'bar'
                                       , 'Bar_default'
                                       , 0
                                       , 'Bar description.'
                                       , 'BAR_ENV'
                                       , 'b'
                                       , 'bar'
                                       )

            registerConfigurationOption( 'baz'
                                       , 0
                                       , 1
                                       , 'Baz description.'
                                       , 'BAZ_ENV'
                                       , 'B'
                                       , 'baz'
                                       )

        def tearDown( self ):
            _clear()

            global BASE_DIR, CONFIG_FILE_NAME
            BASE_DIR =3D self._old_BASE_DIR
            CONFIG_FILE_NAME =3D self._old_CONFIG_FILE_NAME

        def test_Info( self ):

            info =3D getConfigurationInfo( 'foo' )
            self.assertEqual( info.getConfigFileKey(), 'foo' )
            self.assertEqual( info.isSwitch(), 0 )
            self.assertEqual( info.getEnvironmentKey(), 'FOO_ENV' )
            self.assertEqual( info.getShortOption(), 'f' )
            self.assertEqual( info.getLongOption(), 'foo' )
            self.assertEqual( info.getDescription(), 'Foo description.' )
            self.assertEqual( info.getValue(), 'Foo_default' )
            self.assertEqual( info.getSource(), DEFAULT )

        def test_Info_env( self ):

            info =3D getConfigurationInfo( 'FOO_ENV' )
            self.assertEqual( info.getConfigFileKey(), 'foo' )
            self.assertEqual( info.isSwitch(), 0 )
            self.assertEqual( info.getEnvironmentKey(), 'FOO_ENV' )
            self.assertEqual( info.getShortOption(), 'f' )
            self.assertEqual( info.getLongOption(), 'foo' )
            self.assertEqual( info.getDescription(), 'Foo description.' )
            self.assertEqual( info.getValue(), 'Foo_default' )
            self.assertEqual( info.getSource(), DEFAULT )

        def test_ConfigFile( self ):

            import tempfile
            import os
            fn =3D tempfile.mktemp()
            f =3D open( fn, 'w+b' )
            f.write( 'foo =3D "Foo_nondefault"\n' )
            f.write( 'baz =3D 1\n' )
            f.close()

            parseConfigFile( fn )
            os.unlink( fn )

            self.assertEqual( getConfigurationValue( 'foo' ), 'Foo_nondefau=
lt' )
            self.assertEqual( getConfigurationSource( 'foo' ), CONFIG_FILE =
)
            self.assertEqual( getConfigurationValue( 'bar' ), 'Bar_default'=
 )
            self.assertEqual( getConfigurationSource( 'bar' ), DEFAULT )
            self.assertEqual( getConfigurationValue( 'baz' ), 1 )
            self.assertEqual( getConfigurationSource( 'baz' ), CONFIG_FILE =
)

        def test_Environment( self ):

            env =3D { 'FOO_ENV' : 'Foo_nondefault'
                  , 'BAZ_ENV' : 1
                  }
            parseEnvironment( env )

            self.assertEqual( getConfigurationValue( 'foo' ), 'Foo_nondefau=
lt' )
            self.assertEqual( getConfigurationSource( 'foo' ), ENVIRONMENT =
)
            self.assertEqual( getConfigurationValue( 'bar' ), 'Bar_default'=
 )
            self.assertEqual( getConfigurationSource( 'bar' ), DEFAULT )
            self.assertEqual( getConfigurationValue( 'baz' ), 1 )
            self.assertEqual( getConfigurationSource( 'baz' ), ENVIRONMENT =
)

        def test_CommandLine( self ):

            argv =3D ( '-f', 'Foo_nondefault', '--baz' )
            parseCommandLine( argv )

            self.assertEqual( getConfigurationValue( 'foo' ), 'Foo_nondefau=
lt' )
            self.assertEqual( getConfigurationSource( 'foo' ), COMMAND_LINE=
 )
            self.assertEqual( getConfigurationValue( 'bar' ), 'Bar_default'=
 )
            self.assertEqual( getConfigurationSource( 'bar' ), DEFAULT )
            self.assertEqual( getConfigurationValue( 'baz' ), 1 )
            self.assertEqual( getConfigurationSource( 'baz' ), COMMAND_LINE=
 )

        def test_Layered( self ):

            import os
            import sys
            import tempfile

            sys.argv =3D ( '--baz', )
            os.environ =3D { 'FOO_ENV' : 'Foo_nondefault' }

            fn =3D tempfile.mktemp()
            f =3D open( fn, 'w+b' )
            f.write( 'foo =3D "Foo_config_file"\n' )
            f.close()

            global BASE_DIR, CONFIG_FILE_NAME
            BASE_DIR =3D ''
            CONFIG_FILE_NAME =3D fn

            parseOptions()
            os.unlink( fn )

            self.assertEqual( getConfigurationValue( 'foo' ), 'Foo_nondefau=
lt' )
            self.assertEqual( getConfigurationSource( 'foo' ), ENVIRONMENT =
)
            self.assertEqual( getConfigurationValue( 'bar' ), 'Bar_default'=
 )
            self.assertEqual( getConfigurationSource( 'bar' ), DEFAULT )
            self.assertEqual( getConfigurationValue( 'baz' ), 1 )
            self.assertEqual( getConfigurationSource( 'baz' ), COMMAND_LINE=
 )

    unittest.main()

--=-Kl94KR0j8bN0qEF4CB8t--