diff options
| -rw-r--r-- | docs/api/config.rst | 10 | ||||
| -rw-r--r-- | docs/api/paster.rst | 15 | ||||
| -rw-r--r-- | docs/api/scripting.rst | 4 | ||||
| -rw-r--r-- | pyramid/config.py | 4 | ||||
| -rw-r--r-- | pyramid/paster.py | 91 | ||||
| -rw-r--r-- | pyramid/scripting.py | 75 | ||||
| -rw-r--r-- | pyramid/tests/test_config.py | 22 | ||||
| -rw-r--r-- | pyramid/tests/test_paster.py | 41 | ||||
| -rw-r--r-- | pyramid/tests/test_scripting.py | 105 | ||||
| -rw-r--r-- | pyramid/tests/test_util.py | 73 | ||||
| -rw-r--r-- | pyramid/util.py | 49 |
11 files changed, 429 insertions, 60 deletions
diff --git a/docs/api/config.rst b/docs/api/config.rst index 71ef4a746..d021412b8 100644 --- a/docs/api/config.rst +++ b/docs/api/config.rst @@ -86,3 +86,13 @@ .. automethod:: testing_add_renderer + .. attribute:: global_registries + + A set of registries that have been created for :app:`Pyramid` + applications. 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..3e9a814fc 100644 --- a/docs/api/scripting.rst +++ b/docs/api/scripting.rst @@ -7,3 +7,7 @@ .. autofunction:: get_root + .. autofunction:: get_root2 + + .. autofunction:: make_request + 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..578b854a5 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 get_root2 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,43 @@ 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. + + .. 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) + root, closer = get_root2(request) + return { + 'app': app, + 'root': root, + 'closer': closer, + } + _marker = object() class PCommand(Command): @@ -56,18 +94,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 +145,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 +223,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 +257,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 +292,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 +504,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..bdc287c83 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,59 @@ def get_root(app, request=None): root = app.root_factory(request) return root, closer +def get_root2(request=None, registry=None): + """ Return a tuple composed of ``(root, closer)``. The ``root`` + returned is the application's 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 None, a default one is constructed using + :meth:`pyramid.scripting.make_request`. It is used as the request + passed to the :app:`Pyramid` application root factory. + + 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 = 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(request=request): # keep request alive via this function default + threadlocal_manager.pop() + root_factory = registry.queryUtility(IRootFactory, + default=DefaultRootFactory) + root = root_factory(request) + return root, closer + +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..df55afcb8 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_getroot2 = pyramid.paster.get_root2 + 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 DummyGetRoot2(object): + def __call__(self, *a, **kw): + self.a = a + self.kw = kw + return (root, lambda: None) + self.getroot = pyramid.paster.get_root2 = DummyGetRoot2() + + def tearDown(self): + import pyramid.paster + pyramid.paster.get_app = self.original_get_app + pyramid.paster.get_root2 = self.original_getroot2 + + 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..50dbaae85 100644 --- a/pyramid/tests/test_scripting.py +++ b/pyramid/tests/test_scripting.py @@ -2,11 +2,11 @@ import unittest class TestGetRoot(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,108 @@ 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 TestGetRoot2(unittest.TestCase): + def _callFUT(self, request=None, registry=None): + from pyramid.scripting import get_root2 + return get_root2(request, registry) + + def _makeRegistry(self): + return DummyRegistry(DummyFactory) + + def setUp(self): + from pyramid.threadlocal import manager + self.manager = manager + self.default = manager.get() + + def tearDown(self): + self.assertEqual(self.default, self.manager.get()) + + def test_it_norequest(self): + registry = self._makeRegistry() + root, closer = self._callFUT(registry=registry) + pushed = self.manager.get() + self.assertEqual(pushed['registry'], registry) + self.assertEqual(pushed['request'].registry, registry) + self.assertEqual(root.a, (pushed['request'],)) + closer() + + def test_it_withrequest(self): + request = DummyRequest({}) + registry = request.registry = self._makeRegistry() + root, closer = self._callFUT(request) + 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() + + def test_it_with_request_and_registry(self): + request = DummyRequest({}) + registry = request.registry = self._makeRegistry() + root, closer = self._callFUT(request, registry) + 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() + +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]() |
