summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.txt33
-rw-r--r--docs/api/config.rst12
-rw-r--r--docs/api/paster.rst15
-rw-r--r--docs/api/scripting.rst4
-rw-r--r--docs/narr/assets.rst2
-rw-r--r--docs/whatsnew-1.1.rst39
-rw-r--r--pyramid/config.py4
-rw-r--r--pyramid/paster.py94
-rw-r--r--pyramid/scripting.py87
-rw-r--r--pyramid/tests/test_config.py22
-rw-r--r--pyramid/tests/test_paster.py41
-rw-r--r--pyramid/tests/test_scripting.py110
-rw-r--r--pyramid/tests/test_util.py73
-rw-r--r--pyramid/util.py49
14 files changed, 523 insertions, 62 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index bc906772d..0e1f67cdc 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -16,6 +16,39 @@ Features
``pyramid.static.static_view`` exposes a ``use_subpath`` flag for use when
you want the static view to behave like the older deprecated version.
+- A new API function ``pyramid.paster.bootstrap`` has been added to make
+ writing scripts that bootstrap a Pyramid environment easier, e.g.::
+
+ from pyramid.paster import bootstrap
+ info = bootstrap('/path/to/my/development.ini')
+ request = info['request']
+ print request.route_url('myroute')
+
+- A new API function ``pyramid.scripting.prepare`` has been added. It is a
+ lower-level analogue of ``pyramid.paster.boostrap`` that accepts a request
+ and a registry instead of a config file argument, and is used for the same
+ purpose::
+
+ from pyramid.scripting import prepare
+ info = prepare(registry=myregistry)
+ request = info['request']
+ print request.route_url('myroute')
+
+- A new API function ``pyramid.scripting.make_request`` has been added. The
+ resulting request will have a ``registry`` attribute. It is meant to be
+ used in conjunction with ``pyramid.scripting.prepare`` and/or
+ ``pyramid.paster.bootstrap`` (both of which accept a request as an
+ argument)::
+
+ from pyramid.scripting import make_request
+ request = make_request('/')
+
+- New API attribute ``pyramid.config.global_registries`` is an iterable
+ object that contains references to every Pyramid registry loaded into the
+ current process via ``pyramid.config.Configurator.make_app``. It also has
+ a ``last`` attribute containing the last registry loaded. This is used by
+ the scripting machinery, and is available for introspection.
+
Deprecations
------------
diff --git a/docs/api/config.rst b/docs/api/config.rst
index 71ef4a746..2c394ac41 100644
--- a/docs/api/config.rst
+++ b/docs/api/config.rst
@@ -86,3 +86,15 @@
.. automethod:: testing_add_renderer
+ .. attribute:: global_registries
+
+ The set of registries that have been created for :app:`Pyramid`
+ applications, one per each call to
+ :meth:`pyramid.config.Configurator.make_app` in the current process. The
+ object itself supports iteration and has a ``last`` property containing
+ the last registry loaded.
+
+ The registries contained in this object are stored as weakrefs,
+ thus they will only exist for the lifetime of the actual
+ applications for which they are being used.
+
diff --git a/docs/api/paster.rst b/docs/api/paster.rst
index 6668f3c77..2a32e07e9 100644
--- a/docs/api/paster.rst
+++ b/docs/api/paster.rst
@@ -3,14 +3,15 @@
:mod:`pyramid.paster`
---------------------------
-.. module:: pyramid.paster
+.. automodule:: pyramid.paster
-.. function:: get_app(config_file, name=None)
+ .. function:: get_app(config_uri, name=None)
- Return the WSGI application named ``name`` in the PasteDeploy
- config file ``config_file``.
+ Return the WSGI application named ``name`` in the PasteDeploy
+ config file specified by ``config_uri``.
- If the ``name`` is None, this will attempt to parse the name from
- the ``config_file`` string expecting the format ``ini_file#name``.
- If no name is found, the name will default to "main".
+ If the ``name`` is None, this will attempt to parse the name from
+ the ``config_uri`` string expecting the format ``inifile#name``.
+ If no name is found, the name will default to "main".
+ .. autofunction:: bootstrap
diff --git a/docs/api/scripting.rst b/docs/api/scripting.rst
index 9d5bc2e58..79136a98b 100644
--- a/docs/api/scripting.rst
+++ b/docs/api/scripting.rst
@@ -7,3 +7,7 @@
.. autofunction:: get_root
+ .. autofunction:: prepare
+
+ .. autofunction:: make_request
+
diff --git a/docs/narr/assets.rst b/docs/narr/assets.rst
index d57687477..f35f6dd7d 100644
--- a/docs/narr/assets.rst
+++ b/docs/narr/assets.rst
@@ -312,7 +312,7 @@ its behavior is almost exactly the same once it's configured.
``add_view`` (at least those without a ``route_name``). A
:class:`~pyramid.static.static_view` static view cannot be made
root-relative when you use traversal unless it's registered as a
- :term:`NotFound view`.
+ :term:`Not Found view`.
To serve files within a directory located on your filesystem at
``/path/to/static/dir`` as the result of a "catchall" route hanging from the
diff --git a/docs/whatsnew-1.1.rst b/docs/whatsnew-1.1.rst
index dd4d488a0..32955ab75 100644
--- a/docs/whatsnew-1.1.rst
+++ b/docs/whatsnew-1.1.rst
@@ -266,6 +266,45 @@ Minor Feature Additions
:class:`pyramid.static.static_view` exposes a ``use_subpath`` flag for use
when you want the static view to behave like the older deprecated version.
+- A new API function :func:`pyramid.paster.bootstrap` has been added to make
+ writing scripts that bootstrap a Pyramid environment easier, e.g.:
+
+ .. code-block:: python
+
+ from pyramid.paster import bootstrap
+ info = bootstrap('/path/to/my/development.ini')
+ request = info['request']
+ print request.route_url('myroute')
+
+- A new api function :func:`pyramid.scripting.prepare` has been added. It is
+ a lower-level analogue of :func:`pyramid.paster.boostrap` that accepts a
+ request and a registry instead of a config file argument, and is used for
+ the same purpose:
+
+ .. code-block:: python
+
+ from pyramid.scripting import prepare
+ info = prepare(registry=myregistry)
+ request = info['request']
+ print request.route_url('myroute')
+
+- A new API function :func:`pyramid.scripting.make_request` has been added.
+ The resulting request will have a ``registry`` attribute. It is meant to
+ be used in conjunction with :func:`pyramid.scripting.prepare` and/or
+ :func:`pyramid.paster.bootstrap` (both of which accept a request as an
+ argument):
+
+ .. code-block:: python
+
+ from pyramid.scripting import make_request
+ request = make_request('/')
+
+- New API attribute :attr:`pyramid.config.global_registries` is an iterable
+ object that contains references to every Pyramid registry loaded into the
+ current process via :meth:`pyramid.config.Configurator.make_app`. It also
+ has a ``last`` attribute containing the last registry loaded. This is used
+ by the scripting machinery, and is available for introspection.
+
Backwards Incompatibilities
---------------------------
diff --git a/pyramid/config.py b/pyramid/config.py
index 44ce5110e..0ba7fb995 100644
--- a/pyramid/config.py
+++ b/pyramid/config.py
@@ -82,6 +82,7 @@ from pyramid.traversal import find_interface
from pyramid.traversal import traversal_path
from pyramid.urldispatch import RoutesMapper
from pyramid.util import DottedNameResolver
+from pyramid.util import WeakOrderedSet
from pyramid.view import render_view_to_response
DEFAULT_RENDERERS = (
@@ -981,6 +982,7 @@ class Configurator(object):
self.commit()
from pyramid.router import Router # avoid circdep
app = Router(self.registry)
+ global_registries.add(self.registry)
# We push the registry on to the stack here in case any code
# that depends on the registry threadlocal APIs used in
# listeners subscribed to the IApplicationCreated event.
@@ -989,6 +991,7 @@ class Configurator(object):
self.registry.notify(ApplicationCreated(app))
finally:
self.manager.pop()
+
return app
@action_method
@@ -3318,3 +3321,4 @@ def isexception(o):
(inspect.isclass(o) and (issubclass(o, Exception)))
)
+global_registries = WeakOrderedSet()
diff --git a/pyramid/paster.py b/pyramid/paster.py
index a5cd63dfb..b6d7777c0 100644
--- a/pyramid/paster.py
+++ b/pyramid/paster.py
@@ -9,6 +9,7 @@ from paste.deploy import loadapp
from paste.script.command import Command
from pyramid.scripting import get_root
+from pyramid.scripting import prepare
from pyramid.util import DottedNameResolver
from pyramid.scaffolds import PyramidTemplate # bw compat
@@ -17,17 +18,17 @@ zope.deprecation.deprecated(
'pyramid.scaffolds.PyramidTemplate in Pyramid 1.1'),
)
-def get_app(config_file, name=None, loadapp=loadapp):
+def get_app(config_uri, name=None, loadapp=loadapp):
""" Return the WSGI application named ``name`` in the PasteDeploy
- config file ``config_file``.
+ config file specified by ``config_uri``.
If the ``name`` is None, this will attempt to parse the name from
- the ``config_file`` string expecting the format ``ini_file#name``.
+ the ``config_uri`` string expecting the format ``inifile#name``.
If no name is found, the name will default to "main"."""
- if '#' in config_file:
- path, section = config_file.split('#', 1)
+ if '#' in config_uri:
+ path, section = config_uri.split('#', 1)
else:
- path, section = config_file, 'main'
+ path, section = config_uri, 'main'
if name:
section = name
config_name = 'config:%s' % path
@@ -35,6 +36,46 @@ def get_app(config_file, name=None, loadapp=loadapp):
app = loadapp(config_name, name=section, relative_to=here_dir)
return app
+def bootstrap(config_uri, request=None):
+ """ Load a WSGI application from the PasteDeploy config file specified
+ by ``config_uri``. The environment will be configured as if it is
+ currently serving ``request``, leaving a natural environment in place
+ to write scripts that can generate URLs and utilize renderers.
+
+ This function returns a dictionary with ``app``, ``root`` and ``closer``
+ keys. ``app`` is the WSGI app loaded (based on the ``config_uri``),
+ ``root`` is the traversal root resource of the Pyramid application, and
+ ``closer`` is a parameterless callback that may be called when your
+ script is complete (it pops a threadlocal stack).
+
+ .. note:: Most operations within :app:`Pyramid` expect to be invoked
+ within the context of a WSGI request, thus it's important when
+ loading your application to anchor it when executing scripts
+ and other code that is not normally invoked during active WSGI
+ requests.
+
+ .. note:: For a complex config file containing multiple :app:`Pyramid`
+ applications, this function will setup the environment under
+ the context of the last-loaded :app:`Pyramid` application. You
+ may load a specific application yourself by using the
+ lower-level functions :meth:`pyramid.paster.get_app` and
+ :meth:`pyramid.scripting.get_root2` in conjunction with
+ :attr:`pyramid.config.global_registries`.
+
+ ``config_uri`` -- specifies the PasteDeploy config file to use for the
+ interactive shell. The format is ``inifile#name``. If the name is left
+ off, ``main`` will be assumed.
+
+ ``request`` -- specified to anchor the script to a given set of WSGI
+ parameters. For example, most people would want to specify the host,
+ scheme and port such that their script will generate URLs in relation
+ to those parameters.
+ """
+ app = get_app(config_uri)
+ info = prepare(request)
+ info['app'] = app
+ return info
+
_marker = object()
class PCommand(Command):
@@ -56,18 +97,19 @@ class PShellCommand(PCommand):
This command accepts one positional argument:
- ``config_file#section_name`` -- specifies the PasteDeploy config file
- to use for the interactive shell. If the section_name is left off,
- ``main`` will be assumed.
+ ``config_uri`` -- specifies the PasteDeploy config file to use for the
+ interactive shell. The format is ``inifile#name``. If the name is left
+ off, ``main`` will be assumed.
Example::
$ paster pshell myapp.ini#main
- .. note:: You should use a ``section_name`` that refers to the
- actual ``app`` section in the config file that points at
- your Pyramid app without any middleware wrapping, or this
- command will almost certainly fail.
+ .. note:: If you do not point the loader directly at the section of the
+ ini file containing your :app:`Pyramid` application, the
+ command will attempt to find the app for you. If you are
+ loading a pipeline that contains more than one :app:`Pyramid`
+ application within it, the loader will use the last one.
"""
summary = "Open an interactive shell with a Pyramid application loaded"
@@ -106,10 +148,10 @@ class PShellCommand(PCommand):
IPShell = None
cprt = 'Type "help" for more information.'
banner = "Python %s on %s\n%s" % (sys.version, sys.platform, cprt)
- app_spec = self.args[0]
- config_file = app_spec.split('#', 1)[0]
+ config_uri = self.args[0]
+ config_file = config_uri.split('#', 1)[0]
self.logging_file_config(config_file)
- app = self.get_app(app_spec, loadapp=self.loadapp[0])
+ app = self.get_app(config_uri, loadapp=self.loadapp[0])
# load default globals
shell_globals = {
@@ -184,9 +226,9 @@ class PRoutesCommand(PCommand):
This command accepts one positional argument:
- ``config_file#section_name`` -- specifies the PasteDeploy config file
- to use for the interactive shell. If the section_name is left off,
- ``main`` will be assumed.
+ ``config_uri`` -- specifies the PasteDeploy config file to use for the
+ interactive shell. The format is ``inifile#name``. If the name is left
+ off, ``main`` will be assumed.
Example::
@@ -218,8 +260,8 @@ class PRoutesCommand(PCommand):
from pyramid.interfaces import IViewClassifier
from pyramid.interfaces import IView
from zope.interface import Interface
- app_spec = self.args[0]
- app = self.get_app(app_spec, loadapp=self.loadapp[0])
+ config_uri = self.args[0]
+ app = self.get_app(config_uri, loadapp=self.loadapp[0])
registry = app.registry
mapper = self._get_mapper(app)
if mapper is not None:
@@ -253,9 +295,9 @@ class PViewsCommand(PCommand):
This command accepts two positional arguments:
- ``config_file#section_name`` -- specifies the PasteDeploy config file
- to use for the interactive shell. If the section_name is left off,
- ``main`` will be assumed.
+ ``config_uri`` -- specifies the PasteDeploy config file to use for the
+ interactive shell. The format is ``inifile#name``. If the name is left
+ off, ``main`` will be assumed.
``url`` -- specifies the URL that will be used to find matching views.
@@ -465,10 +507,10 @@ class PViewsCommand(PCommand):
self.out("%sview predicates (%s)" % (indent, predicate_text))
def command(self):
- app_spec, url = self.args
+ config_uri, url = self.args
if not url.startswith('/'):
url = '/%s' % url
- app = self.get_app(app_spec, loadapp=self.loadapp[0])
+ app = self.get_app(config_uri, loadapp=self.loadapp[0])
registry = app.registry
view = self._find_view(url, registry)
self.out('')
diff --git a/pyramid/scripting.py b/pyramid/scripting.py
index a3ec9bee5..c04915d3a 100644
--- a/pyramid/scripting.py
+++ b/pyramid/scripting.py
@@ -1,21 +1,24 @@
+from pyramid.config import global_registries
from pyramid.request import Request
from pyramid.interfaces import IRequestFactory
+from pyramid.interfaces import IRootFactory
+from pyramid.threadlocal import manager as threadlocal_manager
+from pyramid.traversal import DefaultRootFactory
def get_root(app, request=None):
""" Return a tuple composed of ``(root, closer)`` when provided a
:term:`router` instance as the ``app`` argument. The ``root``
returned is the application root object. The ``closer`` returned
is a callable (accepting no arguments) that should be called when
- your scripting application is finished using the root. If
- ``request`` is not None, it is used as the request passed to the
- :app:`Pyramid` application root factory. A request is
- constructed and passed to the root factory if ``request`` is None."""
+ your scripting application is finished using the root.
+
+ If ``request`` is not None, it is used as the request passed to the
+ :app:`Pyramid` application root factory. A request is constructed
+ using :meth:`pyramid.scripting.make_request` and passed to the root
+ factory if ``request`` is None."""
registry = app.registry
if request is None:
- request_factory = registry.queryUtility(
- IRequestFactory, default=Request)
- request = request_factory.blank('/')
- request.registry = registry
+ request = make_request('/', registry)
threadlocals = {'registry':registry, 'request':request}
app.threadlocal_manager.push(threadlocals)
def closer(request=request): # keep request alive via this function default
@@ -23,3 +26,71 @@ def get_root(app, request=None):
root = app.root_factory(request)
return root, closer
+def prepare(request=None, registry=None):
+ """ This function pushes data onto the Pyramid threadlocal stack (request
+ and registry), making those objects 'current'. It returns a dictionary
+ useful for bootstrapping a Pyramid application in a scripting
+ environment.
+
+ If ``request`` is None, a default request is constructed using
+ :meth:`pyramid.scripting.make_request`. The request passed to the
+ :app:`Pyramid` application root factory to compute the root.
+
+ If ``registry`` is not supplied, the last registry loaded from
+ :attr:`pyramid.config.global_registries` will be used. If you have
+ loaded more than one :app:`Pyramid` application in the current
+ process, you may not want to use the last registry loaded, thus
+ you can search the ``global_registries`` and supply the appropriate
+ one based on your own criteria.
+
+ The function returns a dictionary composed of ``root``, ``closer``,
+ ``registry``, ``request`` and ``root_factory``. The ``root`` returned is
+ the application's root resource object. The ``closer`` returned is a
+ callable (accepting no arguments) that should be called when your
+ scripting application is finished using the root. ``registry`` is the
+ registry object passed or the last registry loaded into
+ :attr:`pyramid.config.global_registries` if no registry is passed.
+ ``request`` is the request object passed or the constructed request if no
+ request is passed. ``root_factory`` is the root factory used to
+ construct the root.
+ """
+ if registry is None:
+ registry = getattr(request, 'registry', global_registries.last)
+ if request is None:
+ request = make_request('/', registry)
+ request.registry = registry
+ threadlocals = {'registry':registry, 'request':request}
+ threadlocal_manager.push(threadlocals)
+ def closer():
+ threadlocal_manager.pop()
+ root_factory = registry.queryUtility(IRootFactory,
+ default=DefaultRootFactory)
+ root = root_factory(request)
+ return {'root':root, 'closer':closer, 'registry':registry,
+ 'request':request, 'root_factory':root_factory}
+
+def make_request(path, registry=None):
+ """ Return a :meth:`pyramid.request.Request` object anchored at a
+ given path. The object returned will be generated from the supplied
+ registry's :term:`Request Factory` using the
+ :meth:`pyramid.interfaces.IRequestFactory.blank` method.
+
+ This request object can be passed to
+ :meth:`pyramid.scripting.get_root` to initialize an application in
+ preparation for executing a script with a proper environment setup.
+ URLs can then be generated with the object, as well as rendering
+ templates.
+
+ If ``registry`` is not supplied, the last registry loaded from
+ :attr:`pyramid.config.global_registries` will be used. If you have
+ loaded more than one :app:`Pyramid` application in the current
+ process, you may not want to use the last registry loaded, thus
+ you can search the ``global_registries`` and supply the appropriate
+ one based on your own criteria.
+ """
+ if registry is None:
+ registry = global_registries.last
+ request_factory = registry.queryUtility(IRequestFactory, default=Request)
+ request = request_factory.blank(path)
+ request.registry = registry
+ return request
diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py
index 860653e6c..489134f00 100644
--- a/pyramid/tests/test_config.py
+++ b/pyramid/tests/test_config.py
@@ -672,6 +672,7 @@ class ConfiguratorTests(unittest.TestCase):
self.assertEqual(len(L), 1)
def test_make_wsgi_app(self):
+ import pyramid.config
from pyramid.router import Router
from pyramid.interfaces import IApplicationCreated
manager = DummyThreadLocalManager()
@@ -683,8 +684,29 @@ class ConfiguratorTests(unittest.TestCase):
self.assertEqual(manager.pushed['registry'], config.registry)
self.assertEqual(manager.pushed['request'], None)
self.assertTrue(manager.popped)
+ self.assertEqual(pyramid.config.global_registries.last, app.registry)
self.assertEqual(len(subscriber), 1)
self.assertTrue(IApplicationCreated.providedBy(subscriber[0]))
+ pyramid.config.global_registries.empty()
+
+ def test_global_registries_empty(self):
+ from pyramid.config import global_registries
+ self.assertEqual(global_registries.last, None)
+
+ def test_global_registries(self):
+ from pyramid.config import global_registries
+ global_registries.empty()
+ config1 = self._makeOne()
+ config1.make_wsgi_app()
+ self.assertEqual(global_registries.last, config1.registry)
+ config2 = self._makeOne()
+ config2.make_wsgi_app()
+ self.assertEqual(global_registries.last, config2.registry)
+ self.assertEqual(list(global_registries),
+ [config1.registry, config2.registry])
+ global_registries.remove(config2.registry)
+ self.assertEqual(global_registries.last, config1.registry)
+ global_registries.empty()
def test_include_with_dotted_name(self):
from pyramid import tests
diff --git a/pyramid/tests/test_paster.py b/pyramid/tests/test_paster.py
index e7a3b7507..7785b006e 100644
--- a/pyramid/tests/test_paster.py
+++ b/pyramid/tests/test_paster.py
@@ -928,8 +928,45 @@ class TestGetApp(unittest.TestCase):
self.assertEqual(loadapp.section_name, 'yourapp')
self.assertEqual(loadapp.relative_to, os.getcwd())
self.assertEqual(result, app)
-
-
+
+class TestBootstrap(unittest.TestCase):
+ def _callFUT(self, config_uri, request=None):
+ from pyramid.paster import bootstrap
+ return bootstrap(config_uri, request)
+
+ def setUp(self):
+ import pyramid.paster
+ self.original_get_app = pyramid.paster.get_app
+ self.original_prepare = pyramid.paster.prepare
+ self.app = app = DummyApp()
+ self.root = root = Dummy()
+
+ class DummyGetApp(object):
+ def __call__(self, *a, **kw):
+ self.a = a
+ self.kw = kw
+ return app
+ self.get_app = pyramid.paster.get_app = DummyGetApp()
+
+ class DummyPrepare(object):
+ def __call__(self, *a, **kw):
+ self.a = a
+ self.kw = kw
+ return {'root':root, 'closer':lambda: None}
+ self.getroot = pyramid.paster.prepare = DummyPrepare()
+
+ def tearDown(self):
+ import pyramid.paster
+ pyramid.paster.get_app = self.original_get_app
+ pyramid.paster.prepare = self.original_prepare
+
+ def test_it_request_with_registry(self):
+ request = DummyRequest({})
+ request.registry = dummy_registry
+ result = self._callFUT('/foo/bar/myapp.ini', request)
+ self.assertEqual(result['app'], self.app)
+ self.assertEqual(result['root'], self.root)
+ self.assert_('closer' in result)
class Dummy:
pass
diff --git a/pyramid/tests/test_scripting.py b/pyramid/tests/test_scripting.py
index d2139b7db..ccc6656df 100644
--- a/pyramid/tests/test_scripting.py
+++ b/pyramid/tests/test_scripting.py
@@ -1,12 +1,12 @@
import unittest
-class TestGetRoot(unittest.TestCase):
+class Test_get_root(unittest.TestCase):
def _callFUT(self, app, request=None):
- from pyramid.paster import get_root
+ from pyramid.scripting import get_root
return get_root(app, request)
def test_it_norequest(self):
- app = DummyApp()
+ app = DummyApp(registry=dummy_registry)
root, closer = self._callFUT(app)
self.assertEqual(len(app.threadlocal_manager.pushed), 1)
pushed = app.threadlocal_manager.pushed[0]
@@ -17,7 +17,7 @@ class TestGetRoot(unittest.TestCase):
self.assertEqual(len(app.threadlocal_manager.popped), 1)
def test_it_withrequest(self):
- app = DummyApp()
+ app = DummyApp(registry=dummy_registry)
request = DummyRequest({})
root, closer = self._callFUT(app, request)
self.assertEqual(len(app.threadlocal_manager.pushed), 1)
@@ -29,37 +29,111 @@ class TestGetRoot(unittest.TestCase):
self.assertEqual(len(app.threadlocal_manager.popped), 1)
def test_it_requestfactory_overridden(self):
- app = DummyApp()
- request = Dummy()
- class DummyFactory(object):
- @classmethod
- def blank(cls, path):
- return request
- registry = DummyRegistry(DummyFactory)
- app.registry = registry
+ app = DummyApp(registry=dummy_registry)
root, closer = self._callFUT(app)
self.assertEqual(len(app.threadlocal_manager.pushed), 1)
pushed = app.threadlocal_manager.pushed[0]
+ self.assertEqual(pushed['request'].environ['path'], '/')
+
+class Test_prepare(unittest.TestCase):
+ def _callFUT(self, request=None, registry=None):
+ from pyramid.scripting import prepare
+ return prepare(request, registry)
+
+ def _makeRegistry(self):
+ return DummyRegistry(DummyFactory)
+
+ def setUp(self):
+ from pyramid.threadlocal import manager
+ self.manager = manager
+ self.default = manager.get()
+
+ def test_it_norequest(self):
+ registry = self._makeRegistry()
+ info = self._callFUT(registry=registry)
+ root, closer = info['root'], info['closer']
+ pushed = self.manager.get()
+ self.assertEqual(pushed['registry'], registry)
+ self.assertEqual(pushed['request'].registry, registry)
+ self.assertEqual(root.a, (pushed['request'],))
+ closer()
+ self.assertEqual(self.default, self.manager.get())
+
+ def test_it_withrequest(self):
+ request = DummyRequest({})
+ registry = request.registry = self._makeRegistry()
+ info = self._callFUT(request=request)
+ root, closer = info['root'], info['closer']
+ pushed = self.manager.get()
+ self.assertEqual(pushed['request'], request)
+ self.assertEqual(pushed['registry'], registry)
+ self.assertEqual(pushed['request'].registry, registry)
+ self.assertEqual(root.a, (request,))
+ closer()
+ self.assertEqual(self.default, self.manager.get())
+
+ def test_it_with_request_and_registry(self):
+ request = DummyRequest({})
+ registry = request.registry = self._makeRegistry()
+ info = self._callFUT(request=request, registry=registry)
+ root, closer = info['root'], info['closer']
+ pushed = self.manager.get()
self.assertEqual(pushed['request'], request)
+ self.assertEqual(pushed['registry'], registry)
+ self.assertEqual(pushed['request'].registry, registry)
+ self.assertEqual(root.a, (request,))
+ closer()
+ self.assertEqual(self.default, self.manager.get())
+
+class TestMakeRequest(unittest.TestCase):
+ def _callFUT(self, path='/', registry=None):
+ from pyramid.scripting import make_request
+ return make_request(path, registry)
+
+ def test_it_with_registry(self):
+ request = self._callFUT('/', dummy_registry)
+ self.assertEqual(request.environ['path'], '/')
+ self.assertEqual(request.registry, dummy_registry)
+
+ def test_it_with_no_registry(self):
+ from pyramid.config import global_registries
+ # keep registry local so that global_registries is cleared after
+ registry = DummyRegistry(DummyFactory)
+ global_registries.add(registry)
+ request = self._callFUT('/hello')
+ self.assertEqual(request.environ['path'], '/hello')
+ self.assertEqual(request.registry, registry)
+ global_registries.empty()
class Dummy:
pass
dummy_root = Dummy()
+class DummyFactory(object):
+ @classmethod
+ def blank(cls, path):
+ req = DummyRequest({'path': path})
+ return req
+
+ def __init__(self, *a, **kw):
+ self.a = a
+ self.kw = kw
+
class DummyRegistry(object):
- def __init__(self, result=None):
- self.result = result
+ def __init__(self, factory=None):
+ self.factory = factory
def queryUtility(self, iface, default=None):
- return self.result or default
+ return self.factory or default
-dummy_registry = DummyRegistry()
+dummy_registry = DummyRegistry(DummyFactory)
class DummyApp:
- def __init__(self):
- self.registry = dummy_registry
+ def __init__(self, registry=None):
self.threadlocal_manager = DummyThreadLocalManager()
+ if registry:
+ self.registry = registry
def root_factory(self, environ):
return dummy_root
diff --git a/pyramid/tests/test_util.py b/pyramid/tests/test_util.py
index 47aab948a..247b61dad 100644
--- a/pyramid/tests/test_util.py
+++ b/pyramid/tests/test_util.py
@@ -175,3 +175,76 @@ class TestDottedNameResolver(unittest.TestCase):
self.assertEqual(typ.package, None)
self.assertEqual(typ.package_name, None)
+class Test_WeakOrderedSet(unittest.TestCase):
+ def _makeOne(self):
+ from pyramid.config import WeakOrderedSet
+ return WeakOrderedSet()
+
+ def test_ctor(self):
+ wos = self._makeOne()
+ self.assertEqual(len(wos), 0)
+ self.assertEqual(wos.last, None)
+
+ def test_add_item(self):
+ wos = self._makeOne()
+ reg = Dummy()
+ wos.add(reg)
+ self.assertEqual(list(wos), [reg])
+ self.assert_(reg in wos)
+ self.assertEqual(wos.last, reg)
+
+ def test_add_multiple_items(self):
+ wos = self._makeOne()
+ reg1 = Dummy()
+ reg2 = Dummy()
+ wos.add(reg1)
+ wos.add(reg2)
+ self.assertEqual(len(wos), 2)
+ self.assertEqual(list(wos), [reg1, reg2])
+ self.assert_(reg1 in wos)
+ self.assert_(reg2 in wos)
+ self.assertEqual(wos.last, reg2)
+
+ def test_add_duplicate_items(self):
+ wos = self._makeOne()
+ reg = Dummy()
+ wos.add(reg)
+ wos.add(reg)
+ self.assertEqual(len(wos), 1)
+ self.assertEqual(list(wos), [reg])
+ self.assert_(reg in wos)
+ self.assertEqual(wos.last, reg)
+
+ def test_weakref_removal(self):
+ wos = self._makeOne()
+ reg = Dummy()
+ wos.add(reg)
+ wos.remove(reg)
+ self.assertEqual(len(wos), 0)
+ self.assertEqual(list(wos), [])
+ self.assertEqual(wos.last, None)
+
+ def test_last_updated(self):
+ wos = self._makeOne()
+ reg = Dummy()
+ reg2 = Dummy()
+ wos.add(reg)
+ wos.add(reg2)
+ wos.remove(reg2)
+ self.assertEqual(len(wos), 1)
+ self.assertEqual(list(wos), [reg])
+ self.assertEqual(wos.last, reg)
+
+ def test_empty(self):
+ wos = self._makeOne()
+ reg = Dummy()
+ reg2 = Dummy()
+ wos.add(reg)
+ wos.add(reg2)
+ wos.empty()
+ self.assertEqual(len(wos), 0)
+ self.assertEqual(list(wos), [])
+ self.assertEqual(wos.last, None)
+
+class Dummy(object):
+ pass
diff --git a/pyramid/util.py b/pyramid/util.py
index 3e6cd2e60..a4b69ed96 100644
--- a/pyramid/util.py
+++ b/pyramid/util.py
@@ -1,5 +1,6 @@
import pkg_resources
import sys
+import weakref
from pyramid.exceptions import ConfigurationError
from pyramid.path import package_of
@@ -143,4 +144,52 @@ class DottedNameResolver(object):
return self._zope_dottedname_style(dotted)
return dotted
+class WeakOrderedSet(object):
+ """ Maintain a set of items.
+ Each item is stored as a weakref to avoid extending their lifetime.
+
+ The values may be iterated over or the last item added may be
+ accessed via the ``last`` property.
+ """
+
+ def __init__(self):
+ self._items = {}
+ self._order = []
+
+ def add(self, item):
+ """ Add an item to the set."""
+ oid = id(item)
+ if oid in self._items:
+ return
+ ref = weakref.ref(item, lambda x: self.remove(item))
+ self._items[oid] = ref
+ self._order.append(oid)
+
+ def remove(self, item):
+ """ Remove an item from the set."""
+ oid = id(item)
+ if oid in self._items:
+ del self._items[oid]
+ self._order.remove(oid)
+
+ def empty(self):
+ """ Clear all objects from the set."""
+ self._items = {}
+ self._order = []
+
+ def __len__(self):
+ return len(self._order)
+
+ def __contains__(self, item):
+ oid = id(item)
+ return oid in self._items
+
+ def __iter__(self):
+ return (self._items[oid]() for oid in self._order)
+
+ @property
+ def last(self):
+ if self._order:
+ oid = self._order[-1]
+ return self._items[oid]()