Repository: Pylons/pyramid

Issue:
make the configurator a context manager
There's some subtle bugs in apps that hit certain APIs during configuration because parts of pyramid rely on threadlocal state that may or may not be available.

An example:

```python
config = Configurator()
config.override_asset(to_override='myapp:locale/', override_with='yourapp:locale/')
config.add_translation_dirs('myapp:locale')
```

The result here is that the translations are still using `myapp:locale` because `add_translation_dirs` resolves those asset paths at config-time. Asset overrides work by using custom `pkg_resources` hooks that only affect `pkg_resources` when a registry is pushed on the threadlocal stack. Normally this would require surrounding relevant code with `config.begin()` and `config.end()`. We've done a poor job of documenting this.

I propose an optional context manager that we can implement easily (we already did something like this for `p.paster.bootstrap` in https://github.com/Pylons/pyramid/pull/2760) in order to manage the lifecycle.

```
with Configurator() as config:
    config.override_asset(to_override='myapp:locale/', override_with='yourapp:locale/')
    config.add_translation_dirs('myapp:locale')
```

related: https://github.com/Pylons/pyramid/pull/2760 https://github.com/Pylons/pyramid/issues/2046


Discussion:

Resolving patch:
diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py
--- a/pyramid/config/__init__.py
+++ b/pyramid/config/__init__.py
@@ -110,6 +110,17 @@ class Configurator(
     A Configurator is used to configure a :app:`Pyramid`
     :term:`application registry`.
 
+    The Configurator lifecycle can be managed by using a context manager to
+    automatically handle calling :meth:`pyramid.config.Configurator.begin` and
+    :meth:`pyramid.config.Configurator.end` as well as
+    :meth:`pyramid.config.Configurator.commit`.
+
+    .. code-block:: python
+
+        with Configurator(settings=settings) as config:
+            config.add_route('home', '/')
+            app = config.make_wsgi_app()
+
     If the ``registry`` argument is not ``None``, it must
     be an instance of the :class:`pyramid.registry.Registry` class
     representing the registry to configure.  If ``registry`` is ``None``, the
@@ -265,6 +276,11 @@ class Configurator(
     .. versionadded:: 1.6
        The ``root_package`` argument.
        The ``response_factory`` argument.
+
+    .. versionadded:: 1.9
+       The ability to use the configurator as a context manager with the
+       ``with``-statement to make threadlocal configuration available for
+       further configuration with an implicit commit.
     """
     manager = manager # for testing injection
     venusian = venusian # for testing injection
@@ -646,12 +662,22 @@ def _set_action_state(self, state):
     _ctx = action_state # bw compat
 
     def commit(self):
-        """ Commit any pending configuration actions. If a configuration
+        """
+        Commit any pending configuration actions. If a configuration
         conflict is detected in the pending configuration actions, this method
         will raise a :exc:`ConfigurationConflictError`; within the traceback
         of this error will be information about the source of the conflict,
         usually including file names and line numbers of the cause of the
-        configuration conflicts."""
+        configuration conflicts.
+
+        .. warning::
+           You should think very carefully before manually invoking
+           ``commit()``. Especially not as part of any reusable configuration
+           methods. Normally it should only be done by an application author at
+           the end of configuration in order to override certain aspects of an
+           addon.
+
+        """
         self.begin()
         try:
             self.action_state.execute_actions(introspector=self.introspector)
@@ -933,6 +959,16 @@ def end(self):
         """
         return self.manager.pop()
 
+    def __enter__(self):
+        self.begin()
+        return self
+
+    def __exit__(self, exc_type, exc_value, exc_traceback):
+        self.end()
+
+        if exc_value is None:
+            self.commit()
+
     # this is *not* an action method (uses caller_package)
     def scan(self, package=None, categories=None, onerror=None, ignore=None,
              **kw):


Test patch:
diff --git a/pyramid/tests/test_config/test_init.py b/pyramid/tests/test_config/test_init.py
--- a/pyramid/tests/test_config/test_init.py
+++ b/pyramid/tests/test_config/test_init.py
@@ -141,6 +141,22 @@ def test_end(self):
         self.assertEqual(manager.pushed, pushed)
         self.assertEqual(manager.popped, True)
 
+    def test_context_manager(self):
+        from pyramid.config import Configurator
+        config = Configurator()
+        manager = DummyThreadLocalManager()
+        config.manager = manager
+        view = lambda r: None
+        with config as ctx:
+            self.assertTrue(config is ctx)
+            self.assertEqual(manager.pushed,
+                             {'registry': config.registry, 'request': None})
+            self.assertFalse(manager.popped)
+            config.add_view(view)
+        self.assertTrue(manager.popped)
+        config.add_view(view)  # did not raise a conflict because of commit
+        config.commit()
+
     def test_ctor_with_package_registry(self):
         import sys
         from pyramid.config import Configurator