"""Configuration system for CherryPy.

Configuration in CherryPy is implemented via dictionaries. Keys are strings
which name the mapped value, which may be of any type.


Architecture
------------

CherryPy Requests are part of an Application, which runs in a global context,
and configuration data may apply to any of those three scopes:

    Global: configuration entries which apply everywhere are stored in
    cherrypy.config.
    
    Application: entries which apply to each mounted application are stored
    on the Application object itself, as 'app.config'. This is a two-level
    dict where each key is a path, or "relative URL" (for example, "/" or
    "/path/to/my/page"), and each value is a config dict. Usually, this
    data is provided in the call to cherrypy.tree.mount(root(), config=conf),
    although you may also use app.merge(conf).
    
    Request: each Request object possesses a single 'Request.config' dict.
    Early in the request process, this dict is populated by merging global
    config entries, Application entries (whose path equals or is a parent
    of Request.path_info), and any config acquired while looking up the
    page handler (see next).


Declaration
-----------

Configuration data may be supplied as a Python dictionary, as a filename,
or as an open file object. When you supply a filename or file, CherryPy
uses Python's builtin ConfigParser; you declare Application config by
writing each path as a section header:

    [/path/to/my/page]
    request.stream = True

To declare global configuration entries, place them in a [global] section.

You may also declare config entries directly on the classes and methods
(page handlers) that make up your CherryPy application via the '_cp_config'
attribute. For example:

    class Demo:
        _cp_config = {'tools.gzip.on': True}
        
        def index(self):
            return "Hello world"
        index.exposed = True
        index._cp_config = {'request.show_tracebacks': False}


Namespaces
----------

Configuration keys are separated into namespaces by the first "." in the key.
Current namespaces:

    engine:     Controls the 'application engine', including autoreload.
                These can only be declared in the global config.
    hooks:      Declares additional request-processing functions.
    log:        Configures the logging for each application.
                These can only be declared in the global or / config.
    request:    Adds attributes to each Request.
    response:   Adds attributes to each Response.
    server:     Controls the default HTTP server via cherrypy.server.
                These can only be declared in the global config.
    tools:      Runs and configures additional request-processing packages.

The only key that does not exist in a namespace is the "environment" entry.
This special entry 'imports' other config entries from a template stored in
cherrypy._cpconfig.environments[environment]. It only applies to the global
config, and only when you use cherrypy.config.update.

You can define your own namespaces to be called at the Global, Application,
or Request level, by adding a named handler to cherrypy.config.namespaces,
app.namespaces, or cherrypy.engine.request_class.namespaces. The name can
be any string, and the handler must be either a callable or a context
manager.
"""

import ConfigParser
import sys
from warnings import warn

import cherrypy


environments = {
    "staging": {
        'engine.autoreload_on': False,
        'engine.checker': None,
        'tools.log_headers.on': False,
        'request.show_tracebacks': False,
        },
    "production": {
        'engine.autoreload_on': False,
        'engine.checker': None,
        'tools.log_headers.on': False,
        'request.show_tracebacks': False,
        'log.screen': False,
        },
    "test_suite": {
        'engine.autoreload_on': False,
        'engine.checker': None,
        'tools.log_headers.on': False,
        'request.show_tracebacks': True,
        'log.screen': False,
        },
    }

def as_dict(config):
    """Return a dict from 'config' whether it is a dict, file, or filename."""
    if isinstance(config, basestring):
        config = _Parser().dict_from_file(config)
    elif hasattr(config, 'read'):
        config = _Parser().dict_from_file(config)
    return config

def merge(base, other):
    """Merge one app config (from a dict, file, or filename) into another.
    
    If the given config is a filename, it will be appended to
    cherrypy.engine.reload_files and monitored for changes.
    """
    if isinstance(other, basestring):
        if other not in cherrypy.engine.reload_files:
            cherrypy.engine.reload_files.append(other)
    
    # Load other into base
    for section, value_map in as_dict(other).iteritems():
        base.setdefault(section, {}).update(value_map)


def _call_namespaces(config, namespaces):
    """Iterate through config and pass it to each namespace.
    
    'config' should be a flat dict, where keys use dots to separate
    namespaces, and values are arbitrary.
    'namespaces' should be a dict whose keys are strings and whose
    values are namespace handlers.
    
    The first name in each config key is used to look up the corresponding
    namespace handler. For example, a config entry of {'tools.gzip.on': v}
    will call the 'tools' namespace handler with the args: ('gzip.on', v)
    
    Each handler may be a bare callable, or it may be a context manager
    with __enter__ and __exit__ methods, in which case the __enter__
    method should return the callable.
    """
    # Separate the given config into namespaces
    ns_confs = {}
    for k in config:
        if "." in k:
            ns, name = k.split(".", 1)
            bucket = ns_confs.setdefault(ns, {})
            bucket[name] = config[k]
    
    # I chose __enter__ and __exit__ so someday this could be
    # rewritten using Python 2.5's 'with' statement:
    # for ns, handler in namespaces.iteritems():
    #     with handler as callable:
    #         for k, v in ns_confs.get(ns, {}).iteritems():
    #             callable(k, v)
    for ns, handler in namespaces.iteritems():
        exit = getattr(handler, "__exit__", None)
        if exit:
            callable = handler.__enter__()
            no_exc = True
            try:
                try:
                    for k, v in ns_confs.get(ns, {}).iteritems():
                        callable(k, v)
                except:
                    # The exceptional case is handled here
                    no_exc = False
                    if exit is None:
                        raise
                    if not exit(*sys.exc_info()):
                        raise
                    # The exception is swallowed if exit() returns true
            finally:
                # The normal and non-local-goto cases are handled here
                if no_exc and exit:
                    exit(None, None, None)
        else:
            for k, v in ns_confs.get(ns, {}).iteritems():
                handler(k, v)


class Config(dict):
    """The 'global' configuration data for the entire CherryPy process."""
    
    defaults = {
        'tools.log_tracebacks.on': True,
        'tools.log_headers.on': True,
        'tools.trailing_slash.on': True,
        }
    
    namespaces = {"server": lambda k, v: setattr(cherrypy.server, k, v),
                  "engine": lambda k, v: setattr(cherrypy.engine, k, v),
                  "log": lambda k, v: setattr(cherrypy.log, k, v),
                  }
    
    def __init__(self):
        self.reset()
    
    def reset(self):
        """Reset self to default values."""
        self.clear()
        dict.update(self, self.defaults)
    
    def update(self, config):
        """Update self from a dict, file or filename."""
        if isinstance(config, basestring):
            # Filename
            if config not in cherrypy.engine.reload_files:
                cherrypy.engine.reload_files.append(config)
            config = _Parser().dict_from_file(config)
        elif hasattr(config, 'read'):
            # Open file object
            config = _Parser().dict_from_file(config)
        else:
            config = config.copy()
        
        if isinstance(config.get("global", None), dict):
            if len(config) > 1:
                cherrypy.engine.checker.global_config_contained_paths = True
            config = config["global"]
        
        if 'environment' in config:
            env = environments[config['environment']]
            for k in env:
                if k not in config:
                    config[k] = env[k]
        
        if 'tools.staticdir.dir' in config:
            config['tools.staticdir.section'] = "global"
        
        dict.update(self, config)
        _call_namespaces(config, self.namespaces)
    
    def __setitem__(self, k, v):
        dict.__setitem__(self, k, v)
        _call_namespaces({k: v}, self.namespaces)



class _Parser(ConfigParser.ConfigParser):
    """Sub-class of ConfigParser that keeps the case of options and that raises
    an exception if the file cannot be read.
    """
    
    def optionxform(self, optionstr):
        return optionstr
    
    def read(self, filenames):
        if isinstance(filenames, basestring):
            filenames = [filenames]
        for filename in filenames:
            # try:
            #     fp = open(filename)
            # except IOError:
            #     continue
            fp = open(filename)
            try:
                self._read(fp, filename)
            finally:
                fp.close()
    
    def as_dict(self, raw=False, vars=None):
        """Convert an INI file to a dictionary"""
        # Load INI file into a dict
        from cherrypy.lib import unrepr
        result = {}
        for section in self.sections():
            if section not in result:
                result[section] = {}
            for option in self.options(section):
                value = self.get(section, option, raw, vars)
                try:
                    value = unrepr(value)
                except Exception, x:
                    msg = ("Config error in section: %s, option: %s, value: %s" %
                           (repr(section), repr(option), repr(value)))
                    raise ValueError(msg, x.__class__.__name__, x.args)
                result[section][option] = value
        return result
    
    def dict_from_file(self, file):
        if hasattr(file, 'read'):
            self.readfp(file)
        else:
            self.read(file)
        return self.as_dict()

del ConfigParser
