Commits

Olemis Lang committed ab1d5df Merge

BH Multiproduct #355 : Merge c726f861f0eae8e2c3d484b6 incorporating polymorphic access to section methods while executing Option.__get__ et al.

  • Participants
  • Parent commits c726f86, e8e2c3d
  • Branches t355_trac_test_suite

Comments (0)

Files changed (9)

+#t333/t333_r1427886_schema_autoinc.diff
+#t333/t333_r1427886_mp_gen_schema.diff
+#t115/t115_r1429886_product_envs.diff
+#t115/t115_r1429886_product_config.diff
+#t115/t115_r1429886_product_envs_testing.diff
+#t115/t115_r1431447_product_envs_bep3_p1.diff
+#t115/t115_r1433322_unittest2_assertRaises_msg.diff
+t115/t115_r1436300_option_accessor.diff
+#t355/t355_r1434677_trac_test_attachments.diff
 t355/t355_r1436300_trac_test_ticket_api.diff
-#t355/t355_r1434677_trac_test_attachments.diff

File t115/t115_r1429886_product_config.diff

+# HG changeset patch
+# Parent b4d5205ef4158da8ebeded7c9c939a574a74455c
+BH Multiproduct #115 : Product-specific settings
+
+diff -r b4d5205ef415 bloodhound_dashboard/bhdashboard/model.py
+--- a/bloodhound_dashboard/bhdashboard/model.py	Tue Jan 08 02:01:28 2013 -0500
++++ b/bloodhound_dashboard/bhdashboard/model.py	Tue Jan 08 02:04:21 2013 -0500
+@@ -118,8 +118,9 @@
+                 self._update_from_row(row)
+                 break
+             else:
+-                raise ResourceNotFound('No %(object_name)s with %(where)s' %
+-                                sdata)
++                raise ResourceNotFound(
++                        ('No %(object_name)s with %(where)s' % sdata) 
++                                % tuple(values))
+     
+     def delete(self):
+         """Deletes the matching record from the database"""
+@@ -145,14 +146,15 @@
+                                       for k in self._meta['key_fields']]))):
+             sdata = {'keys':','.join(["%s='%s'" % (k, self._data[k])
+                                      for k in self._meta['key_fields']])}
+-        elif len(self.select(self._env, where =
++        elif self._meta['unique_fields'] and len(self.select(self._env, where =
+                                 dict([(k,self._data[k])
+                                       for k in self._meta['unique_fields']]))):
+             sdata = {'keys':','.join(["%s='%s'" % (k, self._data[k])
+                                      for k in self._meta['unique_fields']])}
+         if sdata:
+             sdata.update(self._meta)
+-            raise TracError('%(object_name)s %(keys)s already exists' %
++            sdata['values'] = self._data
++            raise TracError('%(object_name)s %(keys)s already exists %(values)s' %
+                             sdata)
+             
+         for key in self._meta['key_fields']:
+@@ -208,7 +210,7 @@
+             TicketSystem(self._env).reset_ticket_fields()
+     
+     @classmethod
+-    def select(cls, env, db=None, where=None):
++    def select(cls, env, db=None, where=None, limit=None):
+         """Query the database to get a set of records back"""
+         rows = []
+         fields = cls._meta['key_fields']+cls._meta['non_key_fields']
+@@ -219,7 +221,11 @@
+         wherestr, values = dict_to_kv_str(where)
+         if wherestr:
+             wherestr = ' WHERE ' + wherestr
+-        for row in env.db_query(sql + wherestr, values):
++        if limit is not None:
++            limitstr = ' LIMIT ' + str(int(limit))
++        else:
++            limitstr = ''
++        for row in env.db_query(sql + wherestr + limitstr, values):
+             # we won't know which class we need until called
+             model = cls.__new__(cls)
+             data = dict([(fields[i], row[i]) for i in range(len(fields))])
+diff -r b4d5205ef415 bloodhound_multiproduct/multiproduct/api.py
+--- a/bloodhound_multiproduct/multiproduct/api.py	Tue Jan 08 02:01:28 2013 -0500
++++ b/bloodhound_multiproduct/multiproduct/api.py	Tue Jan 08 02:04:21 2013 -0500
+@@ -31,9 +31,9 @@
+ from trac.util.translation import _, N_
+ from trac.web.chrome import ITemplateProvider
+ 
+-from multiproduct.model import Product, ProductResourceMap
++from multiproduct.model import Product, ProductResourceMap, ProductSetting
+ 
+-DB_VERSION = 2
++DB_VERSION = 3
+ DB_SYSTEM_KEY = 'bloodhound_multi_product_version'
+ PLUGIN_NAME = 'Bloodhound multi product'
+ 
+@@ -43,7 +43,8 @@
+     implements(IEnvironmentSetupParticipant, ITemplateProvider,
+             IPermissionRequestor, ITicketFieldProvider, IResourceManager)
+     
+-    SCHEMA = [mcls._get_schema() for mcls in (Product, ProductResourceMap)]
++    SCHEMA = [mcls._get_schema() \
++            for mcls in (Product, ProductResourceMap, ProductSetting)]
+     del mcls
+     
+     def get_version(self):
+diff -r b4d5205ef415 bloodhound_multiproduct/multiproduct/config.py
+--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
++++ b/bloodhound_multiproduct/multiproduct/config.py	Tue Jan 08 02:04:21 2013 -0500
+@@ -0,0 +1,311 @@
++
++#  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.
++
++"""Configuration objects for Bloodhound product environments"""
++
++__all__ = 'Configuration', 'Section'
++
++import os.path
++
++from trac.config import Configuration, ConfigurationError, Option, Section, \
++        _use_default
++from trac.resource import ResourceNotFound
++from trac.util.text import to_unicode
++
++from multiproduct.model import ProductSetting
++
++class Configuration(Configuration):
++    """Product-aware settings repository equivalent to instances of
++    `trac.config.Configuration` (and thus `ConfigParser` from the
++    Python Standard Library) but retrieving configuration values 
++    from the database.
++    """
++    def __init__(self, env, product, parents=None):
++        """Initialize configuration object with an instance of 
++        `trac.env.Environment` and product prefix.
++
++        Optionally it is possible to inherit settings from parent
++        Configuration objects. Environment's configuration will not
++        be added to parents list.
++        """
++        self.env = env
++        self.product = to_unicode(product)
++        self._sections = {}
++        self._setup_parents(parents)
++
++    def __getitem__(self, name):
++        """Return the configuration section with the specified name.
++        """
++        if name not in self._sections:
++            self._sections[name] = Section(self, name)
++        return self._sections[name]
++
++    def sections(self, compmgr=None, defaults=True):
++        """Return a list of section names.
++
++        If `compmgr` is specified, only the section names corresponding to
++        options declared in components that are enabled in the given
++        `ComponentManager` are returned.
++        """
++        sections = set(to_unicode(s) \
++                for s in ProductSetting.get_sections(self.env, self.product))
++        for parent in self.parents:
++            sections.update(parent.sections(compmgr, defaults=False))
++        if defaults:
++            sections.update(self.defaults(compmgr))
++        return sorted(sections)
++
++    def has_option(self, section, option, defaults=True):
++        """Returns True if option exists in section in either the project
++        trac.ini or one of the parents, or is available through the Option
++        registry.
++
++        (since Trac 0.11)
++        """
++        if ProductSetting.exists(self.env, self.product, section, option):
++            return True
++        for parent in self.parents:
++            if parent.has_option(section, option, defaults=False):
++                return True
++        return defaults and (section, option) in Option.registry
++
++    def save(self):
++        """Nothing to do.
++
++        Notice: Opposite to Trac's Configuration objects Bloodhound's
++        product configuration objects commit changes to the database 
++        immediately. Thus there's no much to do in this method.
++        """
++
++    def parse_if_needed(self, force=False):
++        """Just invalidate options cache.
++
++        Notice: Opposite to Trac's Configuration objects Bloodhound's
++        product configuration objects commit changes to the database 
++        immediately. Thus there's no much to do in this method.
++        """
++        for section in self.sections():
++            self[section]._cache.clear()
++
++    def touch(self):
++        pass
++
++    def set_defaults(self, compmgr=None):
++        """Retrieve all default values and store them explicitly in the
++        configuration, so that they can be saved to file.
++
++        Values already set in the configuration are not overridden.
++        """
++        for section, default_options in self.defaults(compmgr).items():
++            for name, value in default_options.items():
++                if not ProductSetting.exists(self.env, self.product,
++                        section, name):
++                    if any(parent[section].contains(name, defaults=False)
++                           for parent in self.parents):
++                        value = None
++                    self.set(section, name, value)
++
++    # Helper methods
++
++    def _setup_parents(self, parents=None):
++        """Inherit configuration from parent `Configuration` instances.
++        If there's a value set to 'file' option in 'inherit' section then
++        it will be considered as a list of paths to .ini files
++        that will be added to parents list as well.
++        """
++        from trac import config
++        self.parents = (parents or [])
++        for filename in self.get('inherit', 'file').split(','):
++            filename = Section._normalize_path(filename.strip(), self.env)
++            self.parents.append(config.Configuration(filename))
++
++class Section(Section):
++    """Proxy for a specific configuration section.
++
++    Objects of this class should not be instantiated directly.
++    """
++    __slots__ = ['config', 'name', 'overridden', '_cache']
++
++    def __init__(self, config, name):
++        self.config = config
++        self.name = to_unicode(name)
++        self.overridden = {}
++        self._cache = {}
++
++    @property
++    def env(self):
++        return self.config.env
++
++    @property
++    def product(self):
++        return self.config.product
++
++    def contains(self, key, defaults=True):
++        key = to_unicode(key)
++        if ProductSetting.exists(self.env, self.product, self.name, key):
++            return True
++        for parent in self.config.parents:
++            if parent[self.name].contains(key, defaults=False):
++                return True
++        return defaults and Option.registry.has_key((self.name, key))
++
++    __contains__ = contains
++
++    def iterate(self, compmgr=None, defaults=True):
++        """Iterate over the options in this section.
++
++        If `compmgr` is specified, only return default option values for
++        components that are enabled in the given `ComponentManager`.
++        """
++        options = set()
++        name_str = self.name
++        for setting in ProductSetting.select(self.env,
++                where={'product':self.product, 'section':name_str}):
++            option = to_unicode(setting.option)
++            options.add(option.lower())
++            yield option
++        for parent in self.config.parents:
++            for option in parent[self.name].iterate(defaults=False):
++                loption = option.lower()
++                if loption not in options:
++                    options.add(loption)
++                    yield option
++        if defaults:
++            for section, option in Option.get_registry(compmgr).keys():
++                if section == self.name and option.lower() not in options:
++                    yield option
++
++    __iter__ = iterate
++
++    def __repr__(self):
++        return '<%s [%s , %s]>' % (self.__class__.__name__, \
++                self.product, self.name)
++
++    def get(self, key, default=''):
++        """Return the value of the specified option.
++
++        Valid default input is a string. Returns a string.
++        """
++        cached = self._cache.get(key, _use_default)
++        if cached is not _use_default:
++            return cached
++        name_str = self.name
++        key_str = to_unicode(key)
++        settings = ProductSetting.select(self.env, 
++                where={'product':self.product, 'section':name_str,
++                        'option':key_str})
++        if len(settings) > 0:
++            value = settings[0].value
++        else:
++            for parent in self.config.parents:
++                value = parent[self.name].get(key, _use_default)
++                if value is not _use_default:
++                    break
++            else:
++                if default is not _use_default:
++                    option = Option.registry.get((self.name, key))
++                    value = option.default if option else _use_default
++                else:
++                    value = _use_default
++        if value is _use_default:
++            return default
++        if not value:
++            value = u''
++        elif isinstance(value, basestring):
++            value = to_unicode(value)
++        self._cache[key] = value
++        return value
++
++    def getpath(self, key, default=''):
++        """Return a configuration value as an absolute path.
++
++        Relative paths are resolved relative to `conf` subfolder 
++        of the target global environment. This approach is consistent
++        with TracIni path resolution.
++
++        Valid default input is a string. Returns a normalized path.
++
++        (enabled since Trac 0.11.5)
++        """
++        path = self.get(key, default)
++        if not path:
++            return default
++        return self._normalize_path(path, self.env)
++
++    def remove(self, key):
++        """Delete a key from this section.
++
++        Like for `set()`, the changes won't persist until `save()` gets called.
++        """
++        key_str = to_unicode(key)
++        option_key = {
++                'product' : self.product, 
++                'section' : self.name,
++                'option' : key_str,
++            }
++        try:
++            setting = ProductSetting(self.env, keys=option_key)
++        except ResourceNotFound:
++            self.env.log.warning("No record for product option %s", option_key)
++        else:
++            self._cache.pop(key, None)
++            setting.delete()
++            self.env.log.info("Removing product option %s", option_key)
++
++    def set(self, key, value):
++        """Change a configuration value.
++
++        These changes will be persistent right away.
++        """
++        key_str = to_unicode(key)
++        value_str = to_unicode(value)
++        self._cache.pop(key_str, None)
++        option_key = {
++                'product' : self.product, 
++                'section' : self.name,
++                'option' : key_str,
++            }
++        try:
++            setting = ProductSetting(self.env, option_key)
++        except ResourceNotFound:
++            if value is not None:
++                # Insert new record in the database
++                setting = ProductSetting(self.env)
++                setting._data.update(option_key)
++                setting._data['value'] = value_str
++                self.env.log.debug('Writing option %s', setting._data)
++                setting.insert()
++        else:
++            if value is None:
++                # Delete existing record from the database
++                # FIXME : Why bother with setting overriden
++                self.overridden[key] = True
++                setting.delete()
++            else:
++                # Update existing record
++                setting.value = value
++                setting.update()
++
++    # Helper methods
++
++    @staticmethod
++    def _normalize_path(path, env):
++        if not os.path.isabs(path):
++            path = os.path.join(env.path, 'conf', path)
++        return os.path.normcase(os.path.realpath(path))
++
+diff -r b4d5205ef415 bloodhound_multiproduct/multiproduct/model.py
+--- a/bloodhound_multiproduct/multiproduct/model.py	Tue Jan 08 02:01:28 2013 -0500
++++ b/bloodhound_multiproduct/multiproduct/model.py	Tue Jan 08 02:04:21 2013 -0500
+@@ -18,6 +18,7 @@
+ 
+ """Models to support multi-product"""
+ from datetime import datetime
++from itertools import izip
+ 
+ from trac.core import TracError
+ from trac.resource import Resource
+@@ -27,6 +28,10 @@
+ 
+ from bhdashboard.model import ModelBase
+ 
++# -------------------------------------------
++# Product API
++# -------------------------------------------
++
+ 
+ class Product(ModelBase):
+     """The Product table"""
+@@ -37,12 +42,12 @@
+             'no_change_fields':['prefix',],
+             'unique_fields':['name'],
+             }
+-    
++
+     @property
+     def resource(self):
+         """Allow Product to be treated as a Resource"""
+         return Resource('product', self.prefix)
+-    
++
+     def delete(self, resources_to=None):
+         """ override the delete method so that we can move references to this
+         object to a new product """
+@@ -59,7 +64,7 @@
+         for prm in ProductResourceMap.select(self._env, where=where):
+             prm._data['product_id'] = resources_to
+             prm.update()
+-    
++
+     def _update_relations(self, db=None, author=None):
+         """Extra actions due to update"""
+         # tickets need to be updated
+@@ -71,7 +76,7 @@
+             for t in Product.get_tickets(self._env, self._data['prefix']):
+                 ticket = Ticket(self._env, t['id'], db)
+                 ticket.save_changes(author, comment, now)
+-    
++
+     @classmethod
+     def get_tickets(cls, env, product=''):
+         """Retrieve all tickets associated with the product."""
+@@ -88,7 +93,7 @@
+             'unique_fields':[],
+             'auto_inc_fields': ['id'],
+             }
+-    
++
+     def reparent_resource(self, product=None):
+         """a specific function to update a record when it is to move product"""
+         if product is not None:
+@@ -101,3 +106,39 @@
+         self._data['product_id'] = product
+         self.update()
+ 
++# -------------------------------------------
++# Configuration
++# -------------------------------------------
++
++class ProductSetting(ModelBase):
++    """The Product configuration table
++    """
++    _meta = {'table_name':'bloodhound_productconfig',
++            'object_name':'ProductSetting',
++            'key_fields':['product', 'section', 'option'],
++            'non_key_fields':['value', ],
++            'no_change_fields':['product', 'section', 'option'],
++            'unique_fields':[],
++            }
++
++    @classmethod
++    def exists(cls, env, product, section=None, option=None, db=None):
++        """Determine whether there are configuration values for
++        product, section, option .
++        """
++        if product is None:
++            raise ValueError("Product prefix required")
++        l = locals()
++        option_subkey = ([c, l[c]] for c in ('product', 'section', 'option'))
++        option_subkey = dict(c for c in option_subkey if c[1] is not None)
++        return len(cls.select(env, db, where=option_subkey, limit=1)) > 0
++
++    @classmethod
++    def get_sections(cls, env, product):
++        """Retrieve configuration sections defined for a product
++        """
++        # FIXME: Maybe something more ORM-ish should be added in ModelBase
++        return [row[0] for row in env.db_query("""SELECT DISTINCT section 
++                FROM bloodhound_productconfig WHERE product = %s""", 
++                (product,)) ]
++

File t115/t115_r1429886_product_envs.diff

+# HG changeset patch
+# Parent d143652322b7ec13c6bd1f0b7aaf7fa01297ba6a
+BH Multiproduct #115 : Product environments
+
+diff -r d143652322b7 bloodhound_multiproduct/multiproduct/env.py
+--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
++++ b/bloodhound_multiproduct/multiproduct/env.py	Thu Jan 03 01:10:05 2013 -0500
+@@ -0,0 +1,476 @@
++
++#  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.
++
++"""Bloodhound product environment and related APIs"""
++
++import os.path
++
++from trac.config import ConfigSection, Option
++from trac.core import Component, ComponentManager, ExtensionPoint, \
++        implements, TracError
++from trac.env import Environment, ISystemInfoProvider
++from trac.util import get_pkginfo, lazy
++from trac.util.compat import sha1
++
++from multiproduct.model import Product
++
++class ProductEnvironment(Component, ComponentManager):
++    """Bloodhound product-aware environment manager.
++
++    Bloodhound encapsulates access to product resources stored inside a
++    Trac environment via product environments. They are compatible lightweight
++    irepresentations of top level environment. 
++
++    Product environments contain among other things:
++
++    * a configuration file, 
++    * product-aware clones of the wiki and ticket attachments files,
++
++    Product environments do not have:
++
++    * product-specific templates and plugins,
++    * a separate database
++    * active participation in database upgrades and other setup tasks
++
++    See https://issues.apache.org/bloodhound/wiki/Proposals/BEP-0003
++    """
++
++    implements(ISystemInfoProvider)
++
++    @property
++    def system_info_providers(self):
++        r"""System info will still be determined by the global environment.
++        """
++        return self.env.system_info_providers
++
++    @property
++    def setup_participants(self):
++        """Setup participants list for product environments will always
++        be empty based on the fact that upgrades will only be handled by
++        the global environment.
++        """
++        return ()
++
++    components_section = ConfigSection('components',
++        """This section is used to enable or disable components
++        provided by plugins, as well as by Trac itself.
++
++        See also: TracIni , TracPlugins
++        """)
++
++    @property
++    def shared_plugins_dir():
++        """Product environments may not add plugins.
++        """
++        return ''
++
++    # TODO: Estimate product base URL considering global base URL, pattern, ...
++    base_url = ''
++
++    # TODO: Estimate product base URL considering global base URL, pattern, ...
++    base_url_for_redirect = ''
++
++    @property
++    def secure_cookies(self):
++        """Restrict cookies to HTTPS connections.
++        """
++        return self.env.secure_cookies
++
++    @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
++
++    @property
++    def project_url(self):
++        """URL of the main project web site, usually the website in
++        which the `base_url` resides. This is used in notification
++        e-mails.
++        """
++        return self.env.project_url
++
++    project_admin = Option('project', 'admin', '',
++        """E-Mail address of the product's leader / administrator.""")
++
++    @property
++    def project_admin_trac_url(self):
++        """Base URL of a Trac instance where errors in this Trac
++        should be reported.
++        """
++        return self.env.project_admin_trac_url
++
++    # FIXME: Should products have different values i.e. config option ?
++    @property
++    def project_footer(self):
++        """Page footer text (right-aligned).
++        """
++        return self.env.project_footer
++
++    project_icon = Option('project', 'icon', 'common/trac.ico',
++        """URL of the icon of the product.""")
++
++    log_type = Option('logging', 'log_type', 'inherit',
++        """Logging facility to use.
++
++        Should be one of (`inherit`, `none`, `file`, `stderr`, 
++        `syslog`, `winlog`).""")
++
++    log_file = Option('logging', 'log_file', 'trac.log',
++        """If `log_type` is `file`, this should be a path to the
++        log-file.  Relative paths are resolved relative to the `log`
++        directory of the environment.""")
++
++    log_level = Option('logging', 'log_level', 'DEBUG',
++        """Level of verbosity in log.
++
++        Should be one of (`CRITICAL`, `ERROR`, `WARN`, `INFO`, `DEBUG`).""")
++
++    log_format = Option('logging', 'log_format', None,
++        """Custom logging format.
++
++        If nothing is set, the following will be used:
++
++        Trac[$(module)s] $(levelname)s: $(message)s
++
++        In addition to regular key names supported by the Python
++        logger library (see
++        http://docs.python.org/library/logging.html), one could use:
++
++        - $(path)s     the path for the current environment
++        - $(basename)s the last path component of the current environment
++        - $(project)s  the project name
++
++        Note the usage of `$(...)s` instead of `%(...)s` as the latter form
++        would be interpreted by the ConfigParser itself.
++
++        Example:
++        `($(thread)d) Trac[$(basename)s:$(module)s] $(levelname)s: $(message)s`
++
++        ''(since 0.10.5)''""")
++
++    def __init__(self, env, product):
++        """Initialize the product environment.
++
++        :param env:     the global Trac environment
++        :param product: product prefix or an instance of
++                        multiproduct.model.Product
++        """
++        ComponentManager.__init__(self)
++
++        if isinstance(product, Product):
++            if product._env is not env:
++                raise ValueError("Product's environment mismatch")
++        elif isinstance(product, basestring):
++            products = Product.select(env, where={'prefix': product})
++            if len(products) == 1 :
++                product = products[0]
++            else:
++                env.log.debug("Products for '%s' : %s",
++                        product, products)
++                raise LookupError("Missing product %s" % (product,))
++
++        self.env = env
++        self.product = product
++        self.systeminfo = []
++        self._href = self._abs_href = None
++
++        self.setup_config()
++
++    # ISystemInfoProvider methods
++
++    def get_system_info(self):
++        return self.env.get_system_info()
++
++    # Same as parent environment's . Avoid duplicated code
++    component_activated = Environment.component_activated.im_func
++    _component_name = Environment._component_name.im_func
++    _component_rules = Environment._component_rules
++    enable_component = Environment.enable_component.im_func
++    get_known_users = Environment.get_known_users.im_func
++    get_systeminfo = Environment.get_system_info.im_func
++    get_repository = Environment.get_repository.im_func
++    is_component_enabled = Environment.is_component_enabled.im_func
++
++    def get_db_cnx(self):
++        """Return a database connection from the connection pool
++
++        :deprecated: Use :meth:`db_transaction` or :meth:`db_query` instead
++
++        `db_transaction` for obtaining the `db` database connection
++        which can be used for performing any query
++        (SELECT/INSERT/UPDATE/DELETE)::
++
++           with env.db_transaction as db:
++               ...
++
++
++        `db_query` for obtaining a `db` database connection which can
++        be used for performing SELECT queries only::
++
++           with env.db_query as db:
++               ...
++        """
++        # TODO: Install database schema proxy with limited scope (see #288)
++        #return DatabaseManager(self).get_connection()
++        raise NotImplementedError
++
++    @lazy
++    def db_exc(self):
++        """Return an object (typically a module) containing all the
++        backend-specific exception types as attributes, named
++        according to the Python Database API
++        (http://www.python.org/dev/peps/pep-0249/).
++
++        To catch a database exception, use the following pattern::
++
++            try:
++                with env.db_transaction as db:
++                    ...
++            except env.db_exc.IntegrityError, e:
++                ...
++        """
++        return DatabaseManager(self).get_exceptions()
++
++    def with_transaction(self, db=None):
++        """Decorator for transaction functions :deprecated:"""
++        # TODO: What shall we do ?
++        #return with_transaction(self, db)
++        raise NotImplementedError
++
++    def get_read_db(self):
++        """Return a database connection for read purposes :deprecated:
++
++        See `trac.db.api.get_read_db` for detailed documentation."""
++        # TODO: Install database schema proxy with limited scope (see #288)
++        #return DatabaseManager(self).get_connection(readonly=True)
++        raise NotImplementedError
++
++    @property
++    def db_query(self):
++        """Return a context manager which can be used to obtain a
++        read-only database connection.
++
++        Example::
++
++            with env.db_query as db:
++                cursor = db.cursor()
++                cursor.execute("SELECT ...")
++                for row in cursor.fetchall():
++                    ...
++
++        Note that a connection retrieved this way can be "called"
++        directly in order to execute a query::
++
++            with env.db_query as db:
++                for row in db("SELECT ..."):
++                    ...
++
++        If you don't need to manipulate the connection itself, this
++        can even be simplified to::
++
++            for row in env.db_query("SELECT ..."):
++                ...
++
++        :warning: after a `with env.db_query as db` block, though the
++          `db` variable is still available, you shouldn't use it as it
++          might have been closed when exiting the context, if this
++          context was the outermost context (`db_query` or
++          `db_transaction`).
++        """
++        # TODO: Install database schema proxy with limited scope (see #288)
++        #return QueryContextManager(self)
++        raise NotImplementedError
++
++    @property
++    def db_transaction(self):
++        """Return a context manager which can be used to obtain a
++        writable database connection.
++
++        Example::
++
++            with env.db_transaction as db:
++                cursor = db.cursor()
++                cursor.execute("UPDATE ...")
++
++        Upon successful exit of the context, the context manager will
++        commit the transaction. In case of nested contexts, only the
++        outermost context performs a commit. However, should an
++        exception happen, any context manager will perform a rollback.
++
++        Like for its read-only counterpart, you can directly execute a
++        DML query on the `db`::
++
++            with env.db_transaction as db:
++                db("UPDATE ...")
++
++        If you don't need to manipulate the connection itself, this
++        can also be simplified to::
++
++            env.db_transaction("UPDATE ...")
++
++        :warning: after a `with env.db_transaction` as db` block,
++          though the `db` variable is still available, you shouldn't
++          use it as it might have been closed when exiting the
++          context, if this context was the outermost context
++          (`db_query` or `db_transaction`).
++        """
++        # TODO: Install database schema proxy with limited scope (see #288)
++        #return TransactionContextManager(self)
++        raise NotImplementedError
++
++    def shutdown(self, tid=None):
++        """Close the environment."""
++        RepositoryManager(self).shutdown(tid)
++        # FIXME: Shared DB so IMO this should not happen ... at least not here
++        #DatabaseManager(self).shutdown(tid)
++        if tid is None:
++            self.log.removeHandler(self._log_handler)
++            self._log_handler.flush()
++            self._log_handler.close()
++            del self._log_handler
++
++    def create(self, options=[]):
++        """Placeholder for compatibility when trying to create the basic 
++        directory structure of the environment, etc ...
++
++        This method does nothing at all.
++        """
++        # TODO: Handle options args
++
++    def get_version(self, db=None, initial=False):
++        """Return the current version of the database.  If the
++        optional argument `initial` is set to `True`, the version of
++        the database used at the time of creation will be returned.
++
++        In practice, for database created before 0.11, this will
++        return `False` which is "older" than any db version number.
++
++        :since: 0.11
++
++        :since 1.0: deprecation warning: the `db` parameter is no
++                    longer used and will be removed in version 1.1.1
++        """
++        return self.env.get_version(db, initial)
++
++    def setup_config(self):
++        """Load the configuration object.
++        """
++        # FIXME: Install product-specific configuration object
++        self.config = self.env.config
++        self.setup_log()
++
++    def get_templates_dir(self):
++        """Return absolute path to the templates directory.
++        """
++        return self.env.get_templates_dir()
++
++    def get_htdocs_dir(self):
++        """Return absolute path to the htdocs directory."""
++        return self.env.get_htdocs_dir()
++
++    def get_log_dir(self):
++        """Return absolute path to the log directory."""
++        return self.env.get_log_dir()
++
++    def setup_log(self):
++        """Initialize the logging sub-system."""
++        from trac.log import logger_handler_factory
++        logtype = self.log_type
++        self.env.log.debug("Log type '%s' for product '%s'", 
++                logtype, self.product.prefix)
++        if logtype == 'inherit':
++            logtype = self.env.log_type
++            logfile = self.env.log_file
++            format = self.env.log_format
++        else:
++            logfile = self.log_file
++            format = self.log_format
++        if logtype == 'file' and not os.path.isabs(logfile):
++            logfile = os.path.join(self.get_log_dir(), logfile)
++        logid = 'Trac.%s.%s' % \
++                (sha1(self.env.path).hexdigest(), self.product.prefix)
++        if format:
++            format = format.replace('$(', '%(') \
++                     .replace('%(path)s', self.path) \
++                     .replace('%(basename)s', os.path.basename(self.path)) \
++                     .replace('%(project)s', self.project_name)
++        self.log, self._log_handler = logger_handler_factory(
++            logtype, logfile, self.log_level, logid, format=format)
++
++        from trac import core, __version__ as VERSION
++        self.log.info('-' * 32 + ' environment startup [Trac %s] ' + '-' * 32,
++                      get_pkginfo(core).get('version', VERSION))
++
++    def backup(self, dest=None):
++        """Create a backup of the database.
++
++        :param dest: Destination file; if not specified, the backup is
++                     stored in a file called db_name.trac_version.bak
++        """
++        return self.env.backup(dest)
++
++    def needs_upgrade(self):
++        """Return whether the environment needs to be upgraded."""
++        #for participant in self.setup_participants:
++        #    with self.db_query as db:
++        #        if participant.environment_needs_upgrade(db):
++        #            self.log.warn("Component %s requires environment upgrade",
++        #                          participant)
++        #            return True
++
++        # FIXME: For the time being no need to upgrade the environment
++        # TODO: Determine the role of product environments at upgrade time
++        return False
++
++    def upgrade(self, backup=False, backup_dest=None):
++        """Upgrade database.
++
++        :param backup: whether or not to backup before upgrading
++        :param backup_dest: name of the backup file
++        :return: whether the upgrade was performed
++        """
++        # (Database) upgrades handled by global environment
++        # FIXME: True or False ?
++        return True
++
++    @property
++    def href(self):
++        """The application root path"""
++        if not self._href:
++            self._href = Href(urlsplit(self.abs_href.base)[2])
++        return self._href
++
++    @property
++    def abs_href(self):
++        """The application URL"""
++        if not self._abs_href:
++            if not self.base_url:
++                self.log.warn("base_url option not set in configuration, "
++                              "generated links may be incorrect")
++                self._abs_href = Href('')
++            else:
++                self._abs_href = Href(self.base_url)
++        return self._abs_href
++

File t115/t115_r1429886_product_envs_testing.diff

+# HG changeset patch
+# Parent 3ff6f332771a5ea5c61370fcf58e51f3e520969f
+BH Theme #115 : Test cases for product environments
+
+diff -r 3ff6f332771a bloodhound_multiproduct/tests/config.py
+--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
++++ b/bloodhound_multiproduct/tests/config.py	Tue Jan 08 01:46:58 2013 -0500
+@@ -0,0 +1,197 @@
++# -*- coding: utf-8 -*-
++#
++#  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.
++
++"""Tests for Apache(TM) Bloodhound's product configuration objects"""
++
++from ConfigParser import ConfigParser
++from itertools import groupby
++import os.path
++import shutil
++from StringIO import StringIO
++import unittest
++
++from trac.config import Option
++from trac.tests.config import ConfigurationTestCase
++from trac.util.text import to_unicode
++
++from multiproduct.api import MultiProductSystem
++from multiproduct.config import Configuration
++from multiproduct.model import Product, ProductSetting
++from tests.env import MultiproductTestCase
++
++class ProductConfigTestCase(ConfigurationTestCase, MultiproductTestCase):
++    r"""Test cases for Trac configuration objects rewritten for product 
++    scope.
++    """
++    def setUp(self):
++        r"""Replace Trac environment with product environment
++        """
++        self.env = self._setup_test_env()
++
++        # Dummy config file, a sibling of trac.ini
++        self.filename = os.path.join(self.env.path, 'conf', 'product.ini')
++        # Ensure conf sub-folder is created
++        os.mkdir(os.path.dirname(self.filename))
++
++        self._upgrade_mp(self.env)
++        self._setup_test_log(self.env)
++        self._load_product_from_data(self.env, self.default_product)
++        self._orig_registry = Option.registry
++        Option.registry = {}
++
++    def tearDown(self):
++        Option.registry = self._orig_registry
++        shutil.rmtree(self.env.path)
++        self.env = None
++
++    def _read(self, parents=None, product=None):
++        r"""Override superclass method by returning product-aware configuration
++        object retrieving settings from the database. Such objects will replace
++        instances of `trac.config.Configuration` used in inherited test cases.
++        """
++        if product is None:
++            product = self.default_product
++        return Configuration(self.env, product, parents)
++
++    def _write(self, lines, product=None):
++        r"""Override superclass method by writing configuration values
++        to the database rather than ini file in the filesystem.
++        """
++        if product is None:
++            product = self.default_product
++        product = to_unicode(product)
++        fp = StringIO(('\n'.join(lines + [''])).encode('utf-8'))
++        parser = ConfigParser()
++        parser.readfp(fp, 'bh-product-test')
++        with self.env.db_transaction as db:
++            # Delete existing setting for target product , if any
++            for setting in ProductSetting.select(self.env, db, 
++                    {'product' : product}):
++                setting.delete()
++            # Insert new options
++            for section in parser.sections():
++                option_key = dict(
++                        section=to_unicode(section), 
++                        product=to_unicode(product)
++                    )
++                for option, value in parser.items(section):
++                    option_key.update(dict(option=to_unicode(option)))
++                    setting = ProductSetting(self.env)
++                    setting._data.update(option_key)
++                    setting._data['value'] = to_unicode(value)
++                    setting.insert()
++
++    def _test_with_inherit(self, testcb):
++        """Almost exact copy of `trac.tests.config.ConfigurationTestCase`.
++        Differences explained in inline comments.
++        """
++        # Parent configuration file created in environment's conf sub-folder
++        # PS: This modification would not be necessary if the corresponding
++        #     statement in overriden method would be written the same way
++        #     but the fact that both files have the same parent folder
++        #     is not made obvious in there
++        sitename = os.path.join(os.path.dirname(self.filename), 'trac-site.ini')
++
++        try:
++            with open(sitename, 'w') as sitefile:
++                sitefile.write('[a]\noption = x\n')
++
++            self._write(['[inherit]', 'file = trac-site.ini'])
++            testcb()
++        finally:
++            os.remove(sitename)
++
++    def _dump_settings(self, config):
++        product = config.product
++        fields = ('section', 'option', 'value')
++        rows = [tuple(getattr(s, f, None) for f in fields) for s in 
++                ProductSetting.select(config.env, where={'product' : product})]
++
++        dump = []
++        for section, group in groupby(sorted(rows), lambda row: row[0]):
++            dump.append('[%s]\n' % (section,))
++            for row in group:
++                dump.append('%s = %s\n' % (row[1], row[2]))
++        return dump
++
++    # Test cases rewritten to avoid reading config file. 
++    # It does make sense for product config as it's stored in the database
++
++    def test_set_and_save(self):
++        config = self._read()
++        config.set('b', u'öption0', 'y')
++        config.set(u'aä', 'öption0', 'x')
++        config.set('aä', 'option2', "Voilà l'été")  # UTF-8
++        config.set(u'aä', 'option1', u"Voilà l'été") # unicode
++        # Note: the following would depend on the locale.getpreferredencoding()
++        # config.set('a', 'option3', "Voil\xe0 l'\xe9t\xe9") # latin-1
++        self.assertEquals('x', config.get(u'aä', u'öption0'))
++        self.assertEquals(u"Voilà l'été", config.get(u'aä', 'option1'))
++        self.assertEquals(u"Voilà l'été", config.get(u'aä', 'option2'))
++        config.save()
++
++        dump = self._dump_settings(config)
++        self.assertEquals([
++                           u'[aä]\n',
++                           u"option1 = Voilà l'été\n", 
++                           u"option2 = Voilà l'été\n", 
++                           u'öption0 = x\n', 
++                           # u"option3 = Voilà l'été\n", 
++                           u'[b]\n',
++                           u'öption0 = y\n', 
++                           ],
++                          dump)
++        config2 = self._read()
++        self.assertEquals('x', config2.get(u'aä', u'öption0'))
++        self.assertEquals(u"Voilà l'été", config2.get(u'aä', 'option1'))
++        self.assertEquals(u"Voilà l'été", config2.get(u'aä', 'option2'))
++        # self.assertEquals(u"Voilà l'été", config2.get('a', 'option3'))
++
++    def test_set_and_save_inherit(self):
++        def testcb():
++            config = self._read()
++            config.set('a', 'option2', "Voilà l'été")  # UTF-8
++            config.set('a', 'option1', u"Voilà l'été") # unicode
++            self.assertEquals('x', config.get('a', 'option'))
++            self.assertEquals(u"Voilà l'été", config.get('a', 'option1'))
++            self.assertEquals(u"Voilà l'été", config.get('a', 'option2'))
++            config.save()
++
++            dump = self._dump_settings(config)
++            self.assertEquals([
++                               u'[a]\n',
++                               u"option1 = Voilà l'été\n", 
++                               u"option2 = Voilà l'été\n", 
++                               u'[inherit]\n',
++                               u"file = trac-site.ini\n", 
++                               ],
++                              dump)
++            config2 = self._read()
++            self.assertEquals('x', config2.get('a', 'option'))
++            self.assertEquals(u"Voilà l'été", config2.get('a', 'option1'))
++            self.assertEquals(u"Voilà l'été", config2.get('a', 'option2'))
++        self._test_with_inherit(testcb)
++
++
++def suite():
++    return unittest.makeSuite(ProductConfigTestCase,'test')
++
++if __name__ == '__main__':
++    unittest.main(defaultTest='suite')
++
+diff -r 3ff6f332771a bloodhound_multiproduct/tests/env.py
+--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
++++ b/bloodhound_multiproduct/tests/env.py	Tue Jan 08 01:46:58 2013 -0500
+@@ -0,0 +1,139 @@
++
++#  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.
++
++"""Tests for Apache(TM) Bloodhound's product environments"""
++
++import os.path
++import shutil
++import tempfile
++import unittest
++
++from trac.test import EnvironmentStub
++from trac.tests.env import EnvironmentTestCase
++
++from multiproduct.api import MultiProductSystem
++from multiproduct.env import ProductEnvironment
++from multiproduct.model import Product
++
++# FIXME: Subclass TestCase explictly ?
++class MultiproductTestCase(unittest.TestCase):
++    r"""Mixin providing access to multi-product testing extensions.
++
++    This class serves to the purpose of upgrading existing Trac test cases
++    with multi-product super-powers while still providing the foundations
++    to create product-specific subclasses.
++    """
++
++    # Product data
++
++    default_product = 'tp1'
++    MAX_TEST_PRODUCT = 3
++
++    PRODUCT_DATA = dict(
++            ['tp' + str(i), {'prefix':'tp' + str(i),
++                             'name' : 'test product ' + str(i),
++                             'description' : 'desc for tp' + str(i)}]
++            for i in xrange(1, MAX_TEST_PRODUCT)
++        )
++
++    # Test setup
++
++    def _setup_test_env(self, create_folder=True, path=None):
++        r"""Prepare a new test environment . 
++
++        Optionally set its path to a meaningful location (temp folder
++        if `path` is `None`).
++        """
++        self.env = env = EnvironmentStub(enable=['trac.*', 'multiproduct.*'])
++        if create_folder:
++            if path is None:
++                env.path = tempfile.mkdtemp('bh-product-tempenv')
++            else:
++                env.path = path
++        return env
++
++    def _setup_test_log(self, env):
++        r"""Ensure test product with prefix is loaded
++        """
++        logdir = tempfile.gettempdir()
++        logpath = os.path.join(logdir, 'trac-testing.log')
++        config = env.config
++        config.set('logging', 'log_file', logpath)
++        config.set('logging', 'log_type', 'file')
++        config.set('logging', 'log_level', 'DEBUG')
++        config.save()
++        env.setup_log()
++        env.log.info('%s test case: %s %s',
++                '-' * 10, self.id(), '-' * 10)
++
++    def _load_product_from_data(self, env, prefix):
++        r"""Ensure test product with prefix is loaded
++        """
++        # TODO: Use fixtures implemented in #314
++        product_data = self.PRODUCT_DATA[prefix]
++        product = Product(env)
++        product._data.update(product_data)
++        product.insert()
++
++    def _upgrade_mp(self, env):
++        r"""Apply multi product upgrades
++        """
++        self.mpsystem = MultiProductSystem(env)
++        try:
++            self.mpsystem.upgrade_environment(env.db_transaction)
++        except OperationalError:
++            # table remains but database version is deleted
++            pass
++
++class ProductEnvTestCase(EnvironmentTestCase, MultiproductTestCase):
++    r"""Test cases for Trac environments rewritten for product environments
++    """
++
++    # Test setup
++
++    def setUp(self):
++        r"""Replace Trac environment with product environment
++        """
++        EnvironmentTestCase.setUp(self)
++        try:
++            self.global_env = self.env
++            self._setup_test_log(self.global_env)
++            self._upgrade_mp(self.global_env)
++            self._load_product_from_data(self.global_env, self.default_product)
++            try:
++                self.env = ProductEnvironment(self.global_env, self.default_product)
++            except :
++                # All tests should fail if anything goes wrong
++                self.global_env.log.exception('Error creating product environment')
++                self.env = None
++        except:
++            shutil.rmtree(self.env.path)
++            raise
++
++    def tearDown(self):
++        # Discard product environment
++        self.env = self.global_env
++
++        EnvironmentTestCase.tearDown(self)
++
++def suite():
++    return unittest.makeSuite(ProductEnvTestCase,'test')
++
++if __name__ == '__main__':
++    unittest.main(defaultTest='suite')
++

File t115/t115_r1431447_product_envs_bep3_p1.diff

+# HG changeset patch
+# Parent 9b04a5a324bee6bf4f6b6d044488f9f7868f9e43
+BH Multiproduct #115 : Product environments. Compliance with BEP 3 - part 1
+
+diff -r 9b04a5a324be bloodhound_multiproduct/multiproduct/env.py
+--- a/bloodhound_multiproduct/multiproduct/env.py	Mon Jan 14 14:05:28 2013 +0000
++++ b/bloodhound_multiproduct/multiproduct/env.py	Tue Jan 15 09:25:35 2013 -0500
+@@ -46,7 +46,7 @@
+     def __init__(self, path, create=False, options=[]):
+         super(Environment, self).__init__(path, create=create, options=options)
+         # global environment w/o parent
+-        self.env = None
++        self.parent = None
+         self.product = None
+ 
+     @property
+@@ -82,11 +82,23 @@
+ 
+     implements(trac.env.ISystemInfoProvider)
+ 
+-    @property
+-    def system_info_providers(self):
+-        r"""System info will still be determined by the global environment.
++    def __getattr__(self, attrnm):
++        """Forward attribute access request to parent environment.
++
++        Initially this will affect the following members of
++        `trac.env.Environment` class:
++
++        system_info_providers, secure_cookies, project_admin_trac_url,
++        get_system_info, get_version, get_templates_dir, get_templates_dir,
++        get_log_dir, backup
+         """
+-        return self.env.system_info_providers
++        try:
++            if attrnm == 'parent':
++                raise AttributeError
++            return getattr(self.parent, attrnm)
++        except AttributeError:
++            raise AttributeError("'%s' object has no attribute '%s'" %
++                    (self.__class__.__name__, attrnm))
+ 
+     @property
+     def setup_participants(self):
+@@ -116,12 +128,6 @@
+     base_url_for_redirect = ''
+ 
+     @property
+-    def secure_cookies(self):
+-        """Restrict cookies to HTTPS connections.
+-        """
+-        return self.env.secure_cookies
+-
+-    @property
+     def project_name(self):
+         """Name of the product.
+         """
+@@ -139,24 +145,18 @@
+         which the `base_url` resides. This is used in notification
+         e-mails.
+         """
+-        return self.env.project_url
++        # FIXME: Should products have different values i.e. config option ?
++        return self.parent.project_url
+ 
+     project_admin = Option('project', 'admin', '',
+         """E-Mail address of the product's leader / administrator.""")
+ 
+     @property
+-    def project_admin_trac_url(self):
+-        """Base URL of a Trac instance where errors in this Trac
+-        should be reported.
+-        """
+-        return self.env.project_admin_trac_url
+-
+-    # FIXME: Should products have different values i.e. config option ?
+-    @property
+     def project_footer(self):
+         """Page footer text (right-aligned).
+         """
+-        return self.env.project_footer
++        # FIXME: Should products have different values i.e. config option ?
++        return self.parent.project_footer
+ 
+     project_icon = Option('project', 'icon', 'common/trac.ico',
+         """URL of the icon of the product.""")
+@@ -207,6 +207,13 @@
+         :param product: product prefix or an instance of
+                         multiproduct.model.Product
+         """
++        if not isinstance(env, trac.env.Environment):
++            cls = self.__class__
++            raise TypeError("Initializer must be called with " \
++                "trac.env.Environment instance as first argument " \
++                "(got %s instance instead)" % 
++                         (cls.__module__ + '.' + cls.__name__, ))
++
+         ComponentManager.__init__(self)
+ 
+         if isinstance(product, Product):
+@@ -221,9 +228,9 @@
+                         product, products)
+                 raise LookupError("Missing product %s" % (product,))
+ 
+-        self.env = env
++        self.parent = env
+         self.product = product
+-        self.path = self.env.path
++        self.path = self.parent.path
+         self.systeminfo = []
+         self._href = self._abs_href = None
+ 
+@@ -231,16 +238,12 @@
+ 
+     # ISystemInfoProvider methods
+ 
+-    def get_system_info(self):
+-        return self.env.get_system_info()
+-
+     # Same as parent environment's . Avoid duplicated code
+     component_activated = trac.env.Environment.component_activated.im_func
+     _component_name = trac.env.Environment._component_name.im_func
+     _component_rules = trac.env.Environment._component_rules
+     enable_component = trac.env.Environment.enable_component.im_func
+     get_known_users = trac.env.Environment.get_known_users.im_func
+-    get_systeminfo = trac.env.Environment.get_system_info.im_func
+     get_repository = trac.env.Environment.get_repository.im_func
+     is_component_enabled = trac.env.Environment.is_component_enabled.im_func
+ 
+@@ -264,7 +267,7 @@
+                ...
+         """
+         # share connection pool with global environment
+-        return self.env.get_db_cnx()
++        return self.parent.get_db_cnx()
+ 
+     @lazy
+     def db_exc(self):
+@@ -282,7 +285,7 @@
+                 ...
+         """
+         # exception types same as in global environment
+-        return self.env.db_exc()
++        return self.parent.db_exc()
+ 
+     def with_transaction(self, db=None):
+         """Decorator for transaction functions :deprecated:"""
+@@ -293,7 +296,7 @@
+ 
+         See `trac.db.api.get_read_db` for detailed documentation."""
+         # database connection is shared with global environment
+-        return self.env.get_read_db()
++        return self.parent.get_read_db()
+ 
+     @property
+     def db_query(self):
+@@ -328,7 +331,7 @@
+           `db_transaction`).
+         """
+         BloodhoundIterableCursor.set_env(self)
+-        return QueryContextManager(self.env)
++        return QueryContextManager(self.parent)
+ 
+     @property
+     def db_transaction(self):
+@@ -364,7 +367,7 @@
+           (`db_query` or `db_transaction`).
+         """
+         BloodhoundIterableCursor.set_env(self)
+-        return TransactionContextManager(self.env)
++        return TransactionContextManager(self.parent)
+ 
+     def shutdown(self, tid=None):
+         """Close the environment."""
+@@ -385,58 +388,30 @@
+         """
+         # TODO: Handle options args
+ 
+-    def get_version(self, db=None, initial=False):
+-        """Return the current version of the database.  If the
+-        optional argument `initial` is set to `True`, the version of
+-        the database used at the time of creation will be returned.
+-
+-        In practice, for database created before 0.11, this will
+-        return `False` which is "older" than any db version number.
+-
+-        :since: 0.11
+-
+-        :since 1.0: deprecation warning: the `db` parameter is no
+-                    longer used and will be removed in version 1.1.1
+-        """
+-        return self.env.get_version(db, initial)
+-
+     def setup_config(self):
+         """Load the configuration object.
+         """
+         # FIXME: Install product-specific configuration object
+-        self.config = self.env.config
++        self.config = self.parent.config
+         self.setup_log()
+ 
+-    def get_templates_dir(self):
+-        """Return absolute path to the templates directory.
+-        """
+-        return self.env.get_templates_dir()
+-
+-    def get_htdocs_dir(self):
+-        """Return absolute path to the htdocs directory."""
+-        return self.env.get_htdocs_dir()
+-
+-    def get_log_dir(self):
+-        """Return absolute path to the log directory."""
+-        return self.env.get_log_dir()
+-
+     def setup_log(self):
+         """Initialize the logging sub-system."""
+         from trac.log import logger_handler_factory
+         logtype = self.log_type
+-        self.env.log.debug("Log type '%s' for product '%s'", 
++        self.parent.log.debug("Log type '%s' for product '%s'", 
+                 logtype, self.product.prefix)
+         if logtype == 'inherit':
+-            logtype = self.env.log_type
+-            logfile = self.env.log_file
+-            format = self.env.log_format
++            logtype = self.parent.log_type
++            logfile = self.parent.log_file
++            format = self.parent.log_format
+         else:
+             logfile = self.log_file
+             format = self.log_format
+         if logtype == 'file' and not os.path.isabs(logfile):
+             logfile = os.path.join(self.get_log_dir(), logfile)
+         logid = 'Trac.%s.%s' % \
+-                (sha1(self.env.path).hexdigest(), self.product.prefix)
++                (sha1(self.parent.path).hexdigest(), self.product.prefix)
+         if format:
+             format = format.replace('$(', '%(') \
+                      .replace('%(path)s', self.path) \
+@@ -449,25 +424,8 @@
+         self.log.info('-' * 32 + ' environment startup [Trac %s] ' + '-' * 32,
+                       get_pkginfo(core).get('version', VERSION))
+ 
+-    def backup(self, dest=None):
+-        """Create a backup of the database.
+-
+-        :param dest: Destination file; if not specified, the backup is
+-                     stored in a file called db_name.trac_version.bak
+-        """
+-        return self.env.backup(dest)
+-
+     def needs_upgrade(self):
+         """Return whether the environment needs to be upgraded."""
+-        #for participant in self.setup_participants:
+-        #    with self.db_query as db:
+-        #        if participant.environment_needs_upgrade(db):
+-        #            self.log.warn("Component %s requires environment upgrade",
+-        #                          participant)
+-        #            return True
+-
+-        # FIXME: For the time being no need to upgrade the environment
+-        # TODO: Determine the role of product environments at upgrade time
+         return False
+ 
+     def upgrade(self, backup=False, backup_dest=None):
+diff -r 9b04a5a324be bloodhound_multiproduct/tests/env.py
+--- a/bloodhound_multiproduct/tests/env.py	Mon Jan 14 14:05:28 2013 +0000
++++ b/bloodhound_multiproduct/tests/env.py	Tue Jan 15 09:25:35 2013 -0500
+@@ -18,11 +18,22 @@
+ 
+ """Tests for Apache(TM) Bloodhound's product environments"""
+ 
++from inspect import stack
+ import os.path
+ import shutil
++import sys
+ import tempfile
+-import unittest
++from types import MethodType
+ 
++if sys.version_info < (2, 7):
++    import unittest2 as unittest
++    from unittest2.case import _AssertRaisesContext
++else:
++    import unittest
++    from unittest.case import _AssertRaisesContext
++
++from trac.config import Option
++from trac.env import Environment
+ from trac.test import EnvironmentStub
+ from trac.tests.env import EnvironmentTestCase
+ 
+@@ -39,6 +50,55 @@
+     to create product-specific subclasses.
+     """
+ 
++    # unittest2 extensions
++
++    exceptFailureMessage = None
++
++    class _AssertRaisesLoggingContext(_AssertRaisesContext):
++        """Add logging capabilities to assertRaises
++        """
++        def __init__(self, expected, test_case, expected_regexp=None):
++            _AssertRaisesContext.__init__(
++                    self, expected, test_case, expected_regexp)
++            self.test_case = test_case
++
++        @staticmethod
++        def _tb_locals(tb):
++            if tb is None:
++                # Inspect interpreter stack two levels up
++                ns = stack()[2][0].f_locals.copy()
++            else:
++                # Traceback already in context
++                ns = tb.tb_frame.f_locals.copy()
++            ns.pop('__builtins__', None)
++            return ns
++
++        def __exit__(self, exc_type, exc_value, tb):
++            try:
++                return _AssertRaisesContext.__exit__(self, 
++                    exc_type, exc_value, tb)
++            except self.failureException, exc:
++                msg = self.test_case.exceptFailureMessage 
++                if msg is not None:
++                    standardMsg = str(exc)
++                    msg = msg % self._tb_locals(tb)
++                    msg = self.test_case._formatMessage(msg, standardMsg)
++                    raise self.failureException(msg)
++                else:
++                    raise
++            finally:
++                # Clear message placeholder
++                self.test_case.exceptFailureMessage = None
++
++    def assertRaises(self, excClass, callableObj=None, *args, **kwargs):
++        """Adds logging capabilities on top of unittest2 implementation.
++        """
++        if callableObj is None:
++            return self._AssertRaisesLoggingContext(excClass, self)
++        else:
++            return unittest.TestCase.assertRaises(
++                    self, excClass, callableObj=None, *args, **kwargs)
++
+     # Product data
+ 
+     default_product = 'tp1'
+@@ -100,6 +160,14 @@
+             # table remains but database version is deleted
+             pass
+ 
++    def _mp_setup(self):
++        """Shortcut for quick product-aware environment setup.
++        """
++        self.env = self._setup_test_env()
++        self._upgrade_mp(self.env)
++        self._setup_test_log(self.env)
++        self._load_product_from_data(self.env, self.default_product)
++
+ class ProductEnvTestCase(EnvironmentTestCase, MultiproductTestCase):
+     r"""Test cases for Trac environments rewritten for product environments
+     """
+@@ -131,9 +199,99 @@
+ 
+         EnvironmentTestCase.tearDown(self)
+ 
+-def suite():
+-    return unittest.makeSuite(ProductEnvTestCase,'test')
++class ProductEnvApiTestCase(MultiproductTestCase):
++    """Assertions for Apache(TM) Bloodhound product-specific extensions in
++    [https://issues.apache.org/bloodhound/wiki/Proposals/BEP-0003 BEP 3]
++    """
++    def setUp(self):
++        self._mp_setup()
++        self.product_env = ProductEnvironment(self.env, self.default_product)
++
++    def test_attr_forward_parent(self):
++        class EnvironmentAttrSandbox(EnvironmentStub):
++            """Limit the impact of class edits so as to avoid race conditions
++            """
++
++        self.longMessage = True
++
++        class AttrSuccess(Exception):
++            """Exception raised when target method / property is actually
++            invoked.
++            """
++
++        def property_mock(attrnm, expected_self):
++            def assertAttrFwd(instance):
++                self.assertIs(instance, expected_self, 
++                        "Mismatch in property '%s'" % (attrnm,))
++                raise AttrSuccess
++            return property(assertAttrFwd)
++
++        self.env.__class__ = EnvironmentAttrSandbox
++        try:
++            for attrnm in 'system_info_providers secure_cookies ' \
++                    'project_admin_trac_url get_system_info get_version ' \
++                    'get_templates_dir get_templates_dir get_log_dir ' \
++                    'backup'.split(): 
++                original = getattr(Environment, attrnm)
++                if isinstance(original, MethodType):
++                    translation = getattr(self.product_env, attrnm)
++                    self.assertIs(translation.im_self, self.env,
++                            "'%s' not bound to global env in product env" % 
++                                    (attrnm,))
++                    self.assertIs(translation.im_func, original.im_func,
++                            "'%s' function differs in product env" % (attrnm,))
++                elif isinstance(original, (property, Option)):
++                    # Intercept property access e.g. properties, Option, ...
++                    setattr(self.env.__class__, attrnm, 
++                        property_mock(attrnm, self.env))
++
++                    self.exceptFailureMessage = 'Property %(attrnm)s'
++                    with self.assertRaises(AttrSuccess) as cm_test_attr:
++                        getattr(self.product_env, attrnm)
++                else:
++                    self.fail("Environment member %s has unexpected type" % 
++                            (repr(original),))
++
++        finally:
++            self.env.__class__ = EnvironmentStub
++
++        for attrnm in 'component_activated _component_rules ' \
++                'enable_component get_known_users get_repository ' \
++                'is_component_enabled _component_name'.split():
++            original = getattr(Environment, attrnm)
++            if isinstance(original, MethodType):
++                translation = getattr(self.product_env, attrnm)
++                self.assertIs(translation.im_self, self.product_env,
++                        "'%s' not bound to product env" % (attrnm,))
++                self.assertIs(translation.im_func, original.im_func,
++                        "'%s' function differs in product env" % (attrnm,))
++            elif isinstance(original, property):
++                translation = getattr(ProductEnvironment, attrnm)
++                self.assertIs(original, translation,
++                        "'%s' property differs in product env" % (attrnm,))
++
++    def test_typecheck(self):
++        self._load_product_from_data(self.env, 'tp2')
++        with self.assertRaises(TypeError) as cm_test:
++            new_env = ProductEnvironment(self.product_env, 'tp2')
++
++        msg = str(cm_test.exception)
++        expected_msg = "Initializer must be called with " \
++                "trac.env.Environment instance as first argument " \
++                "(got multiproduct.env.ProductEnvironment instance instead)"
++        self.assertEqual(msg, expected_msg)
++
++    def tearDown(self):
++        # Release reference to transient environment mock object
++        self.env = None
++        self.product_env = None
++
++def test_suite():
++    return unittest.TestSuite([
++            unittest.makeSuite(ProductEnvTestCase,'test'),
++            unittest.makeSuite(ProductEnvApiTestCase, 'test')
++        ])
+ 
+ if __name__ == '__main__':
+-    unittest.main(defaultTest='suite')
++    unittest.main(defaultTest='test_suite')
+ 
+diff -r 9b04a5a324be bloodhound_theme/setup.py
+--- a/bloodhound_theme/setup.py	Mon Jan 14 14:05:28 2013 +0000
++++ b/bloodhound_theme/setup.py	Tue Jan 15 09:25:35 2013 -0500
+@@ -19,6 +19,7 @@
+ #  under the License.
+ 
+ from setuptools import setup
++import sys
+ 
+ setup(
+   name = 'BloodhoundTheme',
+@@ -34,6 +35,7 @@
+       'Framework :: Trac',
+     ],
+   install_requires = ['BloodhoundDashboardPlugin', 'TracThemeEngine', 'Trac'],
++  tests_require = ['unittest2'] if sys.version_info < (2, 7) else [],
+   entry_points = {
+       'trac.plugins': [
+             'bhtheme.theme = bhtheme.theme',

File t115/t115_r1433322_unittest2_assertRaises_msg.diff

+# HG changeset patch
+# Parent 3386dd0e88c375eae35d911970cfa178f49aace7
+BH Multiproduct #115 : Test cases improved. Logging capabilities in assertRaises
+
+diff -r 3386dd0e88c3 bloodhound_multiproduct/multiproduct/env.py
+--- a/bloodhound_multiproduct/multiproduct/env.py	Tue Jan 15 09:32:53 2013 +0000
++++ b/bloodhound_multiproduct/multiproduct/env.py	Tue Jan 15 17:10:55 2013 -0500
+@@ -208,10 +208,11 @@
+                         multiproduct.model.Product
+         """
+         if not isinstance(env, trac.env.Environment):
++            cls = self.__class__
+             raise TypeError("Initializer must be called with " \
+                 "trac.env.Environment instance as first argument " \
+                 "(got %s instance instead)" % 
+-                        (self._component_name(env.__class__),) )
++                         (cls.__module__ + '.' + cls.__name__, ))
+ 
+         ComponentManager.__init__(self)
+ 
+diff -r 3386dd0e88c3 bloodhound_multiproduct/tests/env.py
+--- a/bloodhound_multiproduct/tests/env.py	Tue Jan 15 09:32:53 2013 +0000
++++ b/bloodhound_multiproduct/tests/env.py	Tue Jan 15 17:10:55 2013 -0500
+@@ -18,19 +18,20 @@
+ 
+ """Tests for Apache(TM) Bloodhound's product environments"""
+ 
++from inspect import stack
+ import os.path
+ import shutil
++from sqlite3 import OperationalError
+ import sys
+ import tempfile
+-
+-from sqlite3 import OperationalError
+-
+ from types import MethodType
+ 
+ if sys.version_info < (2, 7):
+     import unittest2 as unittest
++    from unittest2.case import _AssertRaisesContext
+ else:
+     import unittest
++    from unittest.case import _AssertRaisesContext
+ 
+ from trac.config import Option
+ from trac.env import Environment
+@@ -50,6 +51,55 @@
+     to create product-specific subclasses.
+     """
+ 
++    # unittest2 extensions
++
++    exceptFailureMessage = None
++
++    class _AssertRaisesLoggingContext(_AssertRaisesContext):
++        """Add logging capabilities to assertRaises
++        """
++        def __init__(self, expected, test_case, expected_regexp=None):
++            _AssertRaisesContext.__init__(
++                    self, expected, test_case, expected_regexp)
++            self.test_case = test_case
++
++        @staticmethod
++        def _tb_locals(tb):
++            if tb is None:
++                # Inspect interpreter stack two levels up
++                ns = stack()[2][0].f_locals.copy()
++            else:
++                # Traceback already in context
++                ns = tb.tb_frame.f_locals.copy()
++            ns.pop('__builtins__', None)
++            return ns
++
++        def __exit__(self, exc_type, exc_value, tb):
++            try:
++                return _AssertRaisesContext.__exit__(self, 
++                    exc_type, exc_value, tb)
++            except self.failureException, exc:
++                msg = self.test_case.exceptFailureMessage 
++                if msg is not None:
++                    standardMsg = str(exc)
++                    msg = msg % self._tb_locals(tb)
++                    msg = self.test_case._formatMessage(msg, standardMsg)
++                    raise self.failureException(msg)
++                else:
++                    raise
++            finally:
++                # Clear message placeholder
++                self.test_case.exceptFailureMessage = None
++
++    def assertRaises(self, excClass, callableObj=None, *args, **kwargs):
++        """Adds logging capabilities on top of unittest2 implementation.
++        """
++        if callableObj is None:
++            return self._AssertRaisesLoggingContext(excClass, self)
++        else:
++            return unittest.TestCase.assertRaises(
++                    self, excClass, callableObj, *args, **kwargs)
++
+     # Product data
+ 
+     default_product = 'tp1'
+@@ -196,6 +246,7 @@
+                     setattr(self.env.__class__, attrnm, 
+                         property_mock(attrnm, self.env))
+