Commits

Olemis Lang committed b7cc009

BH Multiproduct #115: Product environment and config (unfinished)

Comments (0)

Files changed (2)

t115/t115_r1358855_product_env.diff

+# HG changeset patch
+# Parent 6fb47d85683310114e0a45b0d958d4c38e71d84d
+BH Multiproduct #115 : Barely minimal product-specific environment wrapper
+
+diff --git a/multiproduct/__init__.py b/multiproduct/env.py
+copy from multiproduct/__init__.py
+copy to multiproduct/env.py
+--- a/multiproduct/__init__.py
++++ b/multiproduct/env.py
+@@ -16,8 +16,204 @@
+ #  specific language governing permissions and limitations
+ #  under the License.
+ 
+-"""multiproduct module"""
+-from multiproduct.api import MultiProductSystem
+-from multiproduct.product_admin import ProductAdminPanel
+-import multiproduct.ticket
+-from multiproduct.web_ui import ProductModule
++"""Multiproduct environment model and related APIs."""
++
++from trac.config import Configuration
++from trac.core import Component, ComponentManager
++from trac.env import Environment, ISystemInfoProvider
++
++from multiproduct.model import Product
++
++class ProductEnvironment(Component, ComponentManager):
++    """Bloodhound product environment manager.
++
++    This is a lightweight clone of a Trac environment for a product inside
++    another real Trac environment . This kind of objects act like a wrapper
++    on top of a real environment customizing access to some aspects of the
++    real environment e.g. :
++        * product-specific configuration
++        * user permissions
++    """
++
++    implements(ISystemInfoProvider)
++
++    required = True
++    
++    # Product-specific environment configuration options.
++
++    components_section = ConfigSection('components',
++        """This section is used to enable or disable components
++        provided by plugins, as well as by Trac itself.
++        For further details see documentation for 
++        `trac.env.Environment.components_section` property.
++        """)
++
++    shared_plugins_dir = PathOption('inherit', 'plugins_dir', '',
++        """Path to the //shared plugins directory//.
++        
++        Plugins in that directory are loaded in addition to those in
++        the directory of the environment `plugins`, with this one
++        taking precedence.
++        
++        (''since 0.11'')""")
++
++    base_url = Option('trac', 'base_url', '',
++        """Reference URL for the Trac deployment.
++        
++        This is the base URL that will be used when producing
++        documents that will be used outside of the web browsing
++        context, like for example when inserting URLs pointing to Trac
++        resources in notification e-mails.""")
++
++    base_url_for_redirect = BoolOption('trac', 'use_base_url_for_redirect',
++            False, 
++        """Optionally use `[trac] base_url` for redirects.
++        
++        In some configurations, usually involving running Trac behind
++        a HTTP proxy, Trac can't automatically reconstruct the URL
++        that is used to access it. You may need to use this option to
++        force Trac to use the `base_url` setting also for
++        redirects. This introduces the obvious limitation that this
++        environment will only be usable when accessible from that URL,
++        as redirects are frequently used. ''(since 0.10.5)''""")
++
++    project_url = Option('project', 'url', '',
++        """URL of the main project web site, usually the website in
++        which the `base_url` resides. This is used in notification
++        e-mails.""")
++
++    project_admin = Option('project', 'admin', '',
++        """E-Mail address of the project's administrator.""")
++
++    project_footer = Option('project', 'footer',
++                            N_('Visit Bloodhound open source project at<br />'
++                               '<a href="https://issues.apache.org/bloodhound/">'
++                               'https://issues.apache.org/bloodhound</a>'),
++        """Page footer text (right-aligned).""")
++
++    project_icon = Option('project', 'icon', 'common/trac.ico',
++        """URL of the icon of the project.""")
++
++    # The following options and methods are inherited from real environment
++
++    #secure_cookies = BoolOption('trac', 'secure_cookies', False, ...)
++    #project_admin_trac_url = Option('project', 'admin_trac_url', '.', ...)
++    #log_type = Option('logging', 'log_type', 'none', ...)
++    #log_file = Option('logging', 'log_file', 'trac.log', ...)
++    #log_level = Option('logging', 'log_level', 'DEBUG', ...)
++    #log_format = Option('logging', 'log_format', None, ...)
++    #def get_systeminfo(self):
++    #def _component_name(self, name_or_class):
++    #def verify(self):
++    #def get_db_cnx(self):
++    #def db_exc(self):
++    #def with_transaction(self, db=None):
++    #def get_read_db(self):
++    #def db_query(self):
++    #def db_transaction(self):
++    #def get_version(self, db=None, initial=False):
++    #def get_templates_dir(self):
++    #def get_htdocs_dir(self):
++    #def get_log_dir(self):
++
++    # TODO: What shall we do with these ?
++    #def get_repository(self, reponame=None, authname=None):
++    #def get_known_users(self, cnx=None):
++    #def backup(self, dest=None):
++
++    # Values for the following options are provided by product resource
++
++    @property
++    def project_name(self):
++        """Name of the product.
++        """
++        return self.product.name
++
++    @property
++    def project_description(self):
++        """Short description of the product.
++        """
++        return self.product.description
++
++    # Generic object data model methods
++
++    env = None
++
++    def __init__(self, env, prefix):
++        """Initialize the product environment.
++
++        :param env:     The real Trac environment hosting target product
++        :param prefix:  Prefix of the target product
++        """
++        ComponentManager.__init__(self)
++
++        self.env = env
++        # FIXME : Make this a property to delay loading product object
++        self.product = Product(self.env, dict(prefix=prefix))
++        self._href = self._abs_href = None
++        self.setup_config()
++
++    # On accessing these attributes AttributeError is raised right away
++    GETATTR_IGNORE = ('_rules', 'create')
++
++    def __getattr__(self, attrnm):
++        """Forward attribute access request to real environment.
++        """
++        if attrnm not in self.GETATTR_IGNORE:
++            try:
++                return getattr(self.env, attrnm)
++            except AttributeError:
++                pass
++        raise AttributeError("'%s' object has no attribute '%s'" % \
++                (self.__class__.__name__, attrnm))
++
++    # ISystemInfoProvider methods
++
++    def get_system_info(self):
++        return []
++
++    # Override `trac.env.Environment` methods
++
++    component_activated = Environment.__dict__['component_activated']
++    _component_rules = Environment.__dict__['_component_rules']
++    is_component_enabled = Environment.__dict__['is_component_enabled']
++    enable_component = Environment.__dict__['enable_component']
++
++    def shutdown(self, tid=None):
++        """Close the environment.
++        """
++        self.env.log.warning('Product environment : Ignoring shutdown request')
++
++    def setup_config(self):
++        """Load the configuration file.
++        """
++        product_id = self.product.prefix
++        self.config = Configuration(
++                os.path.join(self.env.path, 'conf', 'product_%s.ini' % product_id),
++                {'envname': product_id})
++        # TODO: Setup trac.ini inheritance
++
++    def setup_log(self):
++        """Skip silently since this is handled by real environment."""
++        # TODO: Do something ? e.g. insert product prefix in log msgs
++
++    def needs_upgrade(self):
++        """Skip silently since upgrades are handled by real environment."""
++
++    def upgrade(self, backup=False, backup_dest=None):
++        """Skip silently since upgrades are handled by real environment."""
++
++    href = Environment.__dict__['href']
++
++    @property
++    def abs_href(self):
++        """The application URL
++        """
++        if not self._abs_href:
++            if not self.base_url:
++                self._abs_href = Href( \
++                        self.env.abs_href('products', self.product.prefix))
++            else:
++                self._abs_href = Href(self.base_url)
++        return self._abs_href
++

t115/t115_r1358855_product_env_config.diff

+# HG changeset patch
+# Parent d35b2c288840e6a0d08f5ee3dccc761fb4a75c1e
+BH Multiproduct #115 : Configuration object for product environment
+
+diff -r d35b2c288840 multiproduct/config.py
+--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
++++ b/multiproduct/config.py	Thu Aug 09 19:29:11 2012 -0400
+@@ -0,0 +1,229 @@
++
++#  Licensed to the Apache Software Foundation (ASF) under one
++#  or more contributor license agreements.  See the NOTICE file
++#  distributed with this work for additional information
++#  regarding copyright ownership.  The ASF licenses this file
++#  to you under the Apache License, Version 2.0 (the
++#  "License"); you may not use this file except in compliance
++#  with the License.  You may obtain a copy of the License at
++#
++#   http://www.apache.org/licenses/LICENSE-2.0
++#
++#  Unless required by applicable law or agreed to in writing,
++#  software distributed under the License is distributed on an
++#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
++#  KIND, either express or implied.  See the License for the
++#  specific language governing permissions and limitations
++#  under the License.
++
++"""Multiproduct environment model and related APIs."""
++
++from ConfigParser import ConfigParser, NoSectionError, NoOptionError
++import os.path
++
++from trac.config import Configuration
++from trac.core import Component, ExtensionPoint, implements, Interface
++
++from multiproduct.model import Product
++
++#------------------------------------------------------
++#    Product configuration API
++#------------------------------------------------------
++
++class IProductConfigStore(Interface):
++    """Loader of product-specific settings.
++    """
++    def load_product_config(product):
++        """Load a `Configuration` object (or equivalent) providing access to
++        product-specific settings. 
++        
++        This method is also responsible for setting up everything so as to 
++        inherit configuration values in environment's configuration file
++        (i.e. trac.ini). 
++
++        :param product: target `Product`
++        """
++
++#------------------------------------------------------
++#    Configuration wrappers
++#------------------------------------------------------
++
++class ConstrainedConfigParser(ConfigParser):
++    """Limit access to configuration values based on rules.
++    """
++    def __init__(self, *args, **kwargs):
++        """Initialize parser object.
++
++        :param rules:   A list of tuples of the form `(section, key, value)`.
++                        Individual rules override access to
++                        configuration option named `key` in `section`. If
++                        `key` is set to `None` the rule is applied for all
++                        options in the given `section` if a more specific
++                        rule cannot be found. 
++
++                        If rule `value` is `None` then all methods will
++                        behave like if option was always missing. 
++
++                        If rule `value` is set to 
++                        `NotImplemented` and `key` is `None` then all 
++                        methods will behave like if section was missing.
++                        The later case takes precedence over option-specific
++                        rules. 
++
++                        If rule `value` is any other object then all
++                        methods will assume option is set to
++                        `unicode(value)` .
++
++        ConfigParser initializer parameters are all supported as well.
++        """
++        self.rules = dict([(s,o), v] for s,o,v in kwargs.pop('rules', []))
++        ConfigParser.__init__(self, *args, **kwargs)
++
++    # Rules management methods
++
++    def add_rule(self, section, option, value):
++        """Add or update a constraint
++        """
++        self.rules[section, option] = value
++
++    def _ignore_section(self, section):
++        """Determine whether a section will be ignored (i.e. there's
++        a rule like `(section, None, NotImplemented)` )
++        """
++        return self.rules.get((section, None)) is NotImplemented
++
++    def _ignore_option(self, section, option):
++        """Determine whether an option will be ignored (i.e. there's
++        a rule like `(section, option, None)` )
++        """
++        class Dummy:
++            pass
++        dummy = Dummy()
++        return any(self.rules.get((section, x), dummy) not in \
++                (None, NotImplemented) for x in [option, None])
++
++    def _option_value(self, section, option, value=None):
++        """Determine option value considering constraints.
++
++        :param section: section name
++        :param option: option name
++        :param value: a shorcut for cases when value is known beforehand
++
++        Caution: This function ignores constraint check
++        """
++        if value is None:
++            value = ConfigParser.get(self, section, option)
++        return unicode(self.rules.get((section, None)) or \
++                self.rules.get((section, option)) or value)
++                
++
++    # ConfigParser methods
++
++    def sections(self):
++        """Return a list of section names, excluding [DEFAULT].
++        Nonetheless , all sections matching rules of the form
++        `(section, None, NotImplemented)` will be removed.
++        """
++        return [s for s in ConfigParser.sections(self) \
++                if not self._ignore_section(s)]
++
++    def has_section(self, section):
++        """Indicate whether the named section is present in the configuration.
++
++        The DEFAULT section is not acknowledged.
++        If there's a rule of the form `(section, None, NotImplemented)` then
++        it won't be acknowledged either.
++        """
++        return ConfigParser.has_section(self, section) and \
++                not self._ignore_section(section)
++
++    def options(self, section):
++        """Return a list of option names for the given section name.
++        
++        If there's a rule of the form `(section, None, NotImplemented)` then
++        `NoSectionError` exception will be raised.
++        If a rule matches an option and evaluates to `None` then the later 
++        will be removed from the list.
++        """
++        if self._ignore_section(section):
++            raise NoSectionError(section)
++        else:
++            return [o for o in ConfigParser.options(self, section) \
++                    if not self._ignore_option(section, o)]
++
++    def get(self, section, option):
++        """Retrieve value for a particular option.
++        Ensure constraints are applied.
++        """
++        if self._ignore_section(section):
++            raise NoSectionError(section)
++        elif self._ignore_option(section, option):
++            raise NoOptionError(section, option)
++        else:
++            return self._option_value(section, option)
++
++    def items(self, section):
++        """Retrieve `(key, value)` pairs for options in a particular section.
++        Ensure constraints are applied.
++        """
++        if self._ignore_section(section):
++            raise NoSectionError(section)
++        else:
++            return [(o,self._option_value(section, o, v)) \
++                    for o,v in ConfigParser.items(self, section) \
++                    if not self._ignore_option(section, o)]
++
++    def has_option(self, section, option):
++        """Check for the existence of a given option in a given section.
++        Check constraints.
++        """
++        return not self._ignore_option(section, option) and \
++                ConfigParser.has_option(self, section, option)
++
++class ConstrainedConfiguration(Configuration):
++    """Limit access to configuration values based on rules.
++    """
++
++    def __init__(self, filename, params=None, rules=None):
++        Configuration.__init__(self, filename, params or {})
++        self.rules = rules
++
++    @property
++    def parser(self):
++        try:
++            return self._parser
++        except AttributeError:
++            self._parser = ConstrainedConfigParser(rules=self.rules)
++            return self._parser
++
++    @parser.setter
++    def parser(self, value):
++        pass
++
++#------------------------------------------------------
++#    Product configuration components
++#------------------------------------------------------
++
++class ProductIniConfig(Component):
++    """Store product configuration in the file system.
++    """
++    implements(IProductConfigStore)
++
++    # IProductConfigStore methods
++
++    def load_product_config(self, product):
++        """Load a `Configuration` object providing access to
++        product-specific settings stored in a file named product_<prefix>.ini
++        located in environment's conf folder.
++        """
++        config_file = os.path.join(product.env.path, 'conf', 
++                'product_%s.ini' % (product.prefix,))
++        PRODUCT_RULES = [
++                # Ignore all options in inherit section ...
++                ('inherit', None, None),
++                # ... inherit settings in trac.ini
++                ('inherit', 'file', 'trac.ini'),
++            ]
++        return ConstrainedConfiguration(config_file, 
++                {'envname' : product.prefix}, PRODUCT_RULES)
++
+diff -r d35b2c288840 multiproduct/env.py
+--- a/multiproduct/env.py	Wed Aug 08 17:09:48 2012 -0400
++++ b/multiproduct/env.py	Thu Aug 09 19:29:11 2012 -0400
+@@ -22,6 +22,7 @@
+ from trac.core import Component, ComponentManager
+ from trac.env import Environment, ISystemInfoProvider
+ 
++from multiproduct.config import IProductConfigStore
+ from multiproduct.model import Product
+ 
+ class ProductEnvironment(Component, ComponentManager):
+@@ -135,6 +136,10 @@
+         """
+         return self.product.description
+ 
++    config_store = ExtensionOption('multiproduct', 'config_store',
++            IProductConfigStore, 'ProductIniConfig',
++            """Configuration store to use to load product settings""")
++
+     # Generic object data model methods
+ 
+     env = None
+@@ -188,10 +193,7 @@
+         """Load the configuration file.
+         """
+         product_id = self.product.prefix
+-        self.config = Configuration(
+-                os.path.join(self.env.path, 'conf', 'product_%s.ini' % product_id),
+-                {'envname': product_id})
+-        # TODO: Setup trac.ini inheritance
++        self.config = self.config_store.load_product_config(self.product)
+ 
+     def setup_log(self):
+         """Skip silently since this is handled by real environment."""