Repository: mmerickel/pyramid_services

Issue:
Singleton per request object
One issue I ran into with your **dbsession** service example (that uses a service factory) is the following:

When looking for the service different sessions are returned depending on the context. This is by design. Citing the documentation:

> The factory will be used at most once per request/context/name combination.

Having different DB session in one request is quite ugly and I can think of no use case for it. How did I end up with different contexts? By using the *request* object outside a view function invoked directly with *pyramid.threadlocal.get_current_request* . In that case the context is None.

This example by [Jon Rosebaugh - What the Zope Transaction Manager Means To Me (and you)](https://metaclassical.com/what-the-zope-transaction-manager-means-to-me-and-you/) would work but I wanted to use *pyramid_services* (for consistency, because I'm already using it in other parts of the application).

I'm aware that this might not be the place for this since it classifies more as a question than an issue, but you could at least fix the example to be more clear that

```python
request.find_service(name='db')
```

will return different sessions depending on the request context.

My workaround is to always pass context=None:

```python
request.find_service(name='db', context=None)
```

Is there a simpler solution (e.g. a singleton per request)? *register_service()* would register a singleton object for the whole application, but I do want a different DB session for every request.

Discussion:

Patch:
diff --git a/CHANGES.txt b/CHANGES.txt
index 8d05981..6240712 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,8 +1,21 @@
 unreleased
 ==========
 
+Backward Incompatibilities
+--------------------------
+
 - Drop Python 3.2 support.
 
+- Use the original service context interface as the cache key instead
+  of the current context. This means the service will be properly created
+  only once for any context satisfying the original interface.
+
+  Previously, if you requested the same service from 2 different contexts
+  in the same request you would receive 2 service objects, instead of
+  a cached version of the original service, assuming the service was
+  registered to satisfy both contexts.
+  See https://github.com/mmerickel/pyramid_services/pull/12
+
 0.3 (2015-12-13)
 ================
 
diff --git a/pyramid_services/__init__.py b/pyramid_services/__init__.py
index 9b80908..197e94a 100644
--- a/pyramid_services/__init__.py
+++ b/pyramid_services/__init__.py
@@ -25,6 +25,11 @@ def includeme(config):
     config.add_directive('register_service_factory', register_service_factory)
     config.add_directive('find_service_factory', find_service_factory)
 
+class ServiceInfo(object):
+    def __init__(self, factory, context_iface):
+        self.factory = factory
+        self.context_iface = context_iface
+
 class SingletonServiceWrapper(object):
     def __init__(self, service):
         self.service = service
@@ -64,13 +69,15 @@ def register_service_factory(
     else:
         context_iface = context
 
+    info = ServiceInfo(service_factory, context_iface)
+
     def register():
         adapters = config.registry.adapters
         adapters.register(
             (IServiceClassifier, context_iface),
             iface,
             name,
-            service_factory,
+            info,
         )
 
     discriminator = ('service factories', (iface, context, name))
@@ -101,11 +108,16 @@ def find_service(request, iface=Interface, context=_marker, name=''):
     svc = cache.lookup(svc_types, iface, name=name, default=None)
     if svc is None:
         adapters = request.registry.adapters
-        svc_factory = adapters.lookup(svc_types, iface, name=name)
-        if svc_factory is None:
+        info = adapters.lookup(svc_types, iface, name=name)
+        if info is None:
             raise ValueError('could not find registered service')
-        svc = svc_factory(context, request)
-        cache.register(svc_types, iface, name, svc)
+        svc = info.factory(context, request)
+        cache.register(
+            (IServiceClassifier, info.context_iface),
+            iface,
+            name,
+            svc,
+        )
     return svc
 
 def find_service_factory(
@@ -118,7 +130,7 @@ def find_service_factory(
     svc_types = (IServiceClassifier, context_iface)
 
     adapters = config_or_request.registry.adapters
-    svc_factory = adapters.lookup(svc_types, iface, name=name)
-    if svc_factory is None:
+    info = adapters.lookup(svc_types, iface, name=name)
+    if info is None:
         raise ValueError('could not find registered service')
-    return svc_factory
+    return info.factory


Test patch:
diff --git a/pyramid_services/tests/test_it.py b/pyramid_services/tests/test_it.py
index 1e482a3..5abeec3 100644
--- a/pyramid_services/tests/test_it.py
+++ b/pyramid_services/tests/test_it.py
@@ -256,6 +256,25 @@ class TestIntegration_register_service_factory(unittest.TestCase):
         self.assertEqual(resp.body, b'foo')
         self.assertEqual(called, [True])
 
+    def test_context_does_not_affect_cache(self):
+        config = self.config
+
+        config.register_service_factory(
+            lambda ctx, req: DummyService('foo'), name='foo')
+
+        def dummy_view(context, request):
+            s1 = request.find_service(name='foo', context=Root())
+            s2 = request.find_service(name='foo', context=Leaf())
+            self.assertTrue(s1 is s2)
+            return s1.result
+
+        config.add_view(dummy_view, renderer='string')
+
+        app = self._makeApp()
+        resp = app.get('/')
+        self.assertEqual(resp.body, b'foo')
+
+
 class TestIntegration_find_service_factory(unittest.TestCase):
     def setUp(self):
         self.config = pyramid.testing.setUp()
@@ -305,12 +324,12 @@ class DummyService(object):
 
 class DummyServiceFactory(object):
     def __init__(self, result):
-        self.result = DummyService(result)
+        self.result = result
 
     def __call__(self, context, request):
         self.context = context
         self.request = request
-        return self.result
+        return DummyService(self.result)
 
 class DummyView(object):
     def __init__(self, *a, **kw):
@@ -318,5 +337,5 @@ class DummyView(object):
         self.kw = kw
 
     def __call__(self, request):
-        svc = request.find_service(*self.a, **self.kw)
-        return svc()
+        self.svc = request.find_service(*self.a, **self.kw)
+        return self.svc()