summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris McDonough <chrism@plope.com>2011-12-04 19:27:46 -0500
committerChris McDonough <chrism@plope.com>2011-12-04 19:27:46 -0500
commit5d462f0c660f939c773862b0fab81728c9ba62c7 (patch)
tree658df249d53de844c2f422ab04c4c26ee4feb86d
parentd5666e630a08c943a22682540aa51174cee6851f (diff)
parenta78b58dd5cf665f7a7aaa18e9e7f6cae3fc7f749 (diff)
downloadpyramid-5d462f0c660f939c773862b0fab81728c9ba62c7.tar.gz
pyramid-5d462f0c660f939c773862b0fab81728c9ba62c7.tar.bz2
pyramid-5d462f0c660f939c773862b0fab81728c9ba62c7.zip
merge feature.introspection branch
-rw-r--r--CHANGES.txt18
-rw-r--r--TODO.txt17
-rw-r--r--docs/api/config.rst17
-rw-r--r--docs/api/interfaces.rst8
-rw-r--r--docs/api/registry.rst30
-rw-r--r--docs/glossary.rst42
-rw-r--r--docs/index.rst6
-rw-r--r--docs/latexindex.rst7
-rw-r--r--docs/narr/advconfig.rst79
-rw-r--r--docs/narr/extconfig.rst366
-rw-r--r--docs/narr/introspector.rst542
-rw-r--r--docs/narr/tb_introspector.pngbin0 -> 46164 bytes
-rw-r--r--pyramid/config/__init__.py341
-rw-r--r--pyramid/config/adapters.py18
-rw-r--r--pyramid/config/assets.py10
-rw-r--r--pyramid/config/factories.py28
-rw-r--r--pyramid/config/i18n.py49
-rw-r--r--pyramid/config/rendering.py17
-rw-r--r--pyramid/config/routes.py47
-rw-r--r--pyramid/config/security.py26
-rw-r--r--pyramid/config/tweens.py28
-rw-r--r--pyramid/config/util.py24
-rw-r--r--pyramid/config/views.py79
-rw-r--r--pyramid/interfaces.py155
-rw-r--r--pyramid/registry.py171
-rw-r--r--pyramid/router.py2
-rw-r--r--pyramid/scripts/pcreate.py6
-rw-r--r--pyramid/tests/test_config/test_init.py478
-rw-r--r--pyramid/tests/test_config/test_routes.py3
-rw-r--r--pyramid/tests/test_config/test_util.py30
-rw-r--r--pyramid/tests/test_config/test_views.py14
-rw-r--r--pyramid/tests/test_events.py5
-rw-r--r--pyramid/tests/test_registry.py351
-rw-r--r--pyramid/tests/test_util.py73
-rw-r--r--pyramid/util.py77
35 files changed, 2837 insertions, 327 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index 8bdb35d6e..41c608af5 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -25,6 +25,19 @@ Features
This function sets up Python logging according to the logging configuration
in a PasteDeploy ini file.
+- Configuration conflict reporting is reported in a more understandable way
+ ("Line 11 in file..." vs. a repr of a tuple of similar info).
+
+- An configuration introspection system was added; see the narrative
+ documentation chapter entitled "Pyramid Configuration Introspection" for
+ more information. New APIs: ``pyramid.registry.Introspectable``,
+ ``pyramid.config.Configurator.introspector``,
+ ``pyramid.config.Configurator.introspectable``,
+ ``pyramid.registry.Registry.introspector``.
+
+- Allow extra keyword arguments to be passed to the
+ ``pyramid.config.Configurator.action`` method.
+
Bug Fixes
---------
@@ -97,6 +110,11 @@ Documentation
- Minor updates to the ZODB Wiki tutorial.
+- A narrative documentation chapter named "Extending Pyramid Configuration"
+ was added; it describes how to add a new directive, and how use the
+ ``pyramid.config.Configurator.action`` method within custom directives. It
+ also describes how to add introspectable objects.
+
Scaffolds
---------
diff --git a/TODO.txt b/TODO.txt
index 92ccf75e0..db7a01a14 100644
--- a/TODO.txt
+++ b/TODO.txt
@@ -6,6 +6,23 @@ Must-Have
- Change starter scaffold to use URL dispatch?
+- Introspection:
+
+ * Review narrative docs.
+
+ * ``default root factory`` category?
+
+ * ``default view mapper`` category?
+
+ * get rid of "tweens" category (can't sort properly?)
+
+ * implement ptweens and proutes based on introspection instead of current
+ state of affairs.
+
+ * introspection hiding for directives?
+
+- Give discriminators a nicer repr for conflict reporting?
+
Nice-to-Have
------------
diff --git a/docs/api/config.rst b/docs/api/config.rst
index a8c193b60..dbfbb1761 100644
--- a/docs/api/config.rst
+++ b/docs/api/config.rst
@@ -94,6 +94,23 @@
.. automethod:: set_renderer_globals_factory(factory)
+ .. attribute:: introspectable
+
+ A shortcut attribute which points to the
+ :class:`pyramid.registry.Introspectable` class (used during
+ directives to provide introspection to actions).
+
+ This attribute is new as of :app:`Pyramid` 1.3.
+
+ .. attribute:: introspector
+
+ The :term:`introspector` related to this configuration. It is an
+ instance implementing the :class:`pyramid.interfaces.IIntrospector`
+ interface. If the Configurator constructor was supplied with an
+ ``introspector`` argument, this attribute will be that value.
+ Otherwise, it will be an instance of a default introspector type.
+
+ This attribute is new as of :app:`Pyramid` 1.3.
.. attribute:: global_registries
diff --git a/docs/api/interfaces.rst b/docs/api/interfaces.rst
index b336e549d..5b190b53b 100644
--- a/docs/api/interfaces.rst
+++ b/docs/api/interfaces.rst
@@ -68,3 +68,11 @@ Other Interfaces
.. autointerface:: IResponse
:members:
+ .. autointerface:: IIntrospectable
+ :members:
+
+ .. autointerface:: IIntrospector
+ :members:
+
+ .. autointerface:: IActionInfo
+ :members:
diff --git a/docs/api/registry.rst b/docs/api/registry.rst
index 4d327370a..25192f3ed 100644
--- a/docs/api/registry.rst
+++ b/docs/api/registry.rst
@@ -14,3 +14,33 @@
accessed as ``request.registry.settings`` or
``config.registry.settings`` in a typical Pyramid application.
+ .. attribute:: introspector
+
+ When a registry is set up (or created) by a :term:`Configurator`, the
+ registry will be decorated with an instance named ``introspector``
+ implementing the :class:`pyramid.interfaces.IIntrospector` interface.
+ See also :attr:`pyramid.config.Configurator.introspector``.
+
+ When a registry is created "by hand", however, this attribute will not
+ exist until set up by a configurator.
+
+ This attribute is often accessed as ``request.registry.introspector`` in
+ a typical Pyramid application.
+
+ This attribute is new as of :app:`Pyramid` 1.3.
+
+.. class:: Introspectable
+
+ The default implementation of the interface
+ :class:`pyramid.interfaces.IIntrospectable` used by framework exenders.
+ An instance of this class is is created when
+ :attr:`pyramid.config.Configurator.introspectable` is called.
+
+ This class is new as of :app:`Pyramid` 1.3.
+
+.. class:: noop_introspector
+
+ An introspector which throws away all registrations, useful for disabling
+ introspection altogether (pass as ``introspector`` to the
+ :term:`Configurator` constructor).
+
diff --git a/docs/glossary.rst b/docs/glossary.rst
index 0d69fbb0d..399b78cdf 100644
--- a/docs/glossary.rst
+++ b/docs/glossary.rst
@@ -563,9 +563,8 @@ Glossary
also `PEP 318 <http://www.python.org/dev/peps/pep-0318/>`_.
configuration declaration
- An individual method call made to an instance of a :app:`Pyramid`
- :term:`Configurator` object which performs an arbitrary action, such as
- registering a :term:`view configuration` (via the
+ An individual method call made to a :term:`configuration directive`,
+ such as registering a :term:`view configuration` (via the
:meth:`~pyramid.config.Configurator.add_view` method of the
configurator) or :term:`route configuration` (via the
:meth:`~pyramid.config.Configurator.add_route` method of the
@@ -941,3 +940,40 @@ Glossary
directory of a Python installation or virtualenv as the result of
running ``setup.py install`` or ``setup.py develop``.
+ introspector
+ An object with the methods described by
+ :class:`pyramid.interfaces.IIntrospector` that is available in both
+ configuration code (for registration) and at runtime (for querying) that
+ allows a developer to introspect configuration statements and
+ relationships between those statements.
+
+ conflict resolution
+ Pyramid attempts to resolve ambiguous configuration statements made by
+ application developers via automatic conflict resolution. Automatic
+ conflict resolution is described in
+ :ref:`automatic_conflict_resolution`. If Pyramid cannot resolve
+ ambiguous configuration statements, it is possible to manually resolve
+ them as described in :ref:`manually_resolving_conflicts`.
+
+ configuration directive
+ A method of the :term:`Configurator` which causes a configuration action
+ to occur. The method :meth:`pyramid.config.Configurator.add_view` is a
+ configuration directive, and application developers can add their own
+ directives as necessary (see :ref:`add_directive`).
+
+ action
+ Represents a pending configuration statement generated by a call to a
+ :term:`configuration directive`. The set of pending configuration
+ actions are processed when :meth:`pyramid.config.Configurator.commit` is
+ called.
+
+ discriminator
+ The unique identifier of an :term:`action`.
+
+ introspectable
+ An object which implements the attributes and methods described in
+ :class:`pyramid.interfaces.IIntrospectable`. Introspectables are used
+ by the :term:`introspector` to display configuration information about
+ a running Pyramid application. An introspectable is associated with a
+ :term:`action` by virtue of the
+ :meth:`pyramid.config.Configurator.action` method.
diff --git a/docs/index.rst b/docs/index.rst
index e4de8b0c8..ceb29d108 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -65,6 +65,7 @@ Narrative documentation in chapter form explaining how to use
narr/configuration
narr/project
narr/startup
+ narr/router
narr/urldispatch
narr/views
narr/renderers
@@ -87,9 +88,10 @@ Narrative documentation in chapter form explaining how to use
narr/security
narr/hybrid
narr/hooks
- narr/advconfig
+ narr/introspector
narr/extending
- narr/router
+ narr/advconfig
+ narr/extconfig
narr/threadlocals
narr/zca
diff --git a/docs/latexindex.rst b/docs/latexindex.rst
index 584dd3825..4db5b64b2 100644
--- a/docs/latexindex.rst
+++ b/docs/latexindex.rst
@@ -31,6 +31,8 @@ Narrative Documentation
narr/configuration
narr/firstapp
narr/project
+ narr/startup
+ narr/router
narr/urldispatch
narr/views
narr/renderers
@@ -53,9 +55,10 @@ Narrative Documentation
narr/security
narr/hybrid
narr/hooks
- narr/advconfig
+ narr/introspector
narr/extending
- narr/startup
+ narr/advconfig
+ narr/extconfig
narr/threadlocals
narr/zca
diff --git a/docs/narr/advconfig.rst b/docs/narr/advconfig.rst
index 7b62b1a73..3a7bf2805 100644
--- a/docs/narr/advconfig.rst
+++ b/docs/narr/advconfig.rst
@@ -87,8 +87,8 @@ that ends something like this:
Conflicting configuration actions
For: ('view', None, '', None, <InterfaceClass pyramid.interfaces.IView>,
None, None, None, None, None, False, None, None, None)
- ('app.py', 14, '<module>', 'config.add_view(hello_world)')
- ('app.py', 17, '<module>', 'config.add_view(hello_world)')
+ Line 14 of file app.py in <module>: 'config.add_view(hello_world)'
+ Line 17 of file app.py in <module>: 'config.add_view(goodbye_world)'
This traceback is trying to tell us:
@@ -115,6 +115,8 @@ Conflict detection happens for any kind of configuration: imperative
configuration or configuration that results from the execution of a
:term:`scan`.
+.. _manually_resolving_conflicts:
+
Manually Resolving Conflicts
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -397,76 +399,3 @@ constraints: the routes they imply require relative ordering. Such ordering
constraints are not absolved by two-phase configuration. Routes are still
added in configuration execution order.
-.. index::
- single: add_directive
- pair: configurator; adding directives
-
-.. _add_directive:
-
-Adding Methods to the Configurator via ``add_directive``
---------------------------------------------------------
-
-Framework extension writers can add arbitrary methods to a
-:term:`Configurator` by using the
-:meth:`pyramid.config.Configurator.add_directive` method of the configurator.
-This makes it possible to extend a Pyramid configurator in arbitrary ways,
-and allows it to perform application-specific tasks more succinctly.
-
-The :meth:`~pyramid.config.Configurator.add_directive` method accepts two
-positional arguments: a method name and a callable object. The callable
-object is usually a function that takes the configurator instance as its
-first argument and accepts other arbitrary positional and keyword arguments.
-For example:
-
-.. code-block:: python
- :linenos:
-
- from pyramid.events import NewRequest
- from pyramid.config import Configurator
-
- def add_newrequest_subscriber(config, subscriber):
- config.add_subscriber(subscriber, NewRequest).
-
- if __name__ == '__main__':
- config = Configurator()
- config.add_directive('add_newrequest_subscriber',
- add_newrequest_subscriber)
-
-Once :meth:`~pyramid.config.Configurator.add_directive` is called, a user can
-then call the method by its given name as if it were a built-in method of the
-Configurator:
-
-.. code-block:: python
- :linenos:
-
- def mysubscriber(event):
- print event.request
-
- config.add_newrequest_subscriber(mysubscriber)
-
-A call to :meth:`~pyramid.config.Configurator.add_directive` is often
-"hidden" within an ``includeme`` function within a "frameworky" package meant
-to be included as per :ref:`including_configuration` via
-:meth:`~pyramid.config.Configurator.include`. For example, if you put this
-code in a package named ``pyramid_subscriberhelpers``:
-
-.. code-block:: python
- :linenos:
-
- def includeme(config)
- config.add_directive('add_newrequest_subscriber',
- add_newrequest_subscriber)
-
-The user of the add-on package ``pyramid_subscriberhelpers`` would then be
-able to install it and subsequently do:
-
-.. code-block:: python
- :linenos:
-
- def mysubscriber(event):
- print event.request
-
- from pyramid.config import Configurator
- config = Configurator()
- config.include('pyramid_subscriberhelpers')
- config.add_newrequest_subscriber(mysubscriber)
diff --git a/docs/narr/extconfig.rst b/docs/narr/extconfig.rst
new file mode 100644
index 000000000..856654377
--- /dev/null
+++ b/docs/narr/extconfig.rst
@@ -0,0 +1,366 @@
+.. index::
+ single: extending configuration
+
+.. _extconfig_narr:
+
+Extending Pyramid Configuration
+===============================
+
+Pyramid allows you to extend its Configurator with custom directives. Custom
+directives can use other directives, they can add a custom :term:`action`,
+they can participate in :term:`conflict resolution`, and they can provide
+some number of :term:`introspectable` objects.
+
+.. index::
+ single: add_directive
+ pair: configurator; adding directives
+
+.. _add_directive:
+
+Adding Methods to the Configurator via ``add_directive``
+--------------------------------------------------------
+
+Framework extension writers can add arbitrary methods to a
+:term:`Configurator` by using the
+:meth:`pyramid.config.Configurator.add_directive` method of the configurator.
+Using :meth:`~pyramid.config.Configurator.add_directive` makes it possible to
+extend a Pyramid configurator in arbitrary ways, and allows it to perform
+application-specific tasks more succinctly.
+
+The :meth:`~pyramid.config.Configurator.add_directive` method accepts two
+positional arguments: a method name and a callable object. The callable
+object is usually a function that takes the configurator instance as its
+first argument and accepts other arbitrary positional and keyword arguments.
+For example:
+
+.. code-block:: python
+ :linenos:
+
+ from pyramid.events import NewRequest
+ from pyramid.config import Configurator
+
+ def add_newrequest_subscriber(config, subscriber):
+ config.add_subscriber(subscriber, NewRequest).
+
+ if __name__ == '__main__':
+ config = Configurator()
+ config.add_directive('add_newrequest_subscriber',
+ add_newrequest_subscriber)
+
+Once :meth:`~pyramid.config.Configurator.add_directive` is called, a user can
+then call the added directive by its given name as if it were a built-in
+method of the Configurator:
+
+.. code-block:: python
+ :linenos:
+
+ def mysubscriber(event):
+ print event.request
+
+ config.add_newrequest_subscriber(mysubscriber)
+
+A call to :meth:`~pyramid.config.Configurator.add_directive` is often
+"hidden" within an ``includeme`` function within a "frameworky" package meant
+to be included as per :ref:`including_configuration` via
+:meth:`~pyramid.config.Configurator.include`. For example, if you put this
+code in a package named ``pyramid_subscriberhelpers``:
+
+.. code-block:: python
+ :linenos:
+
+ def includeme(config)
+ config.add_directive('add_newrequest_subscriber',
+ add_newrequest_subscriber)
+
+The user of the add-on package ``pyramid_subscriberhelpers`` would then be
+able to install it and subsequently do:
+
+.. code-block:: python
+ :linenos:
+
+ def mysubscriber(event):
+ print event.request
+
+ from pyramid.config import Configurator
+ config = Configurator()
+ config.include('pyramid_subscriberhelpers')
+ config.add_newrequest_subscriber(mysubscriber)
+
+Using ``config.action`` in a Directive
+--------------------------------------
+
+If a custom directive can't do its work exclusively in terms of existing
+configurator methods (such as
+:meth:`pyramid.config.Configurator.add_subscriber`, as above), the directive
+may need to make use of the :meth:`pyramid.config.Configurator.action`
+method. This method adds an entry to the list of "actions" that Pyramid will
+attempt to process when :meth:`pyramid.config.Configurator.commit` is called.
+An action is simply a dictionary that includes a :term:`discriminator`,
+possibly a callback function, and possibly other metadata used by Pyramid's
+action system.
+
+Here's an example directive which uses the "action" method:
+
+.. code-block:: python
+ :linenos:
+
+ def add_jammyjam(config, jammyjam):
+ def register():
+ config.registry.jammyjam = jammyjam
+ config.action('jammyjam', register)
+
+ if __name__ == '__main__':
+ config = Configurator()
+ config.add_directive('add_jammyjam', add_jammyjam)
+
+Fancy, but what does it do? The action method accepts a number of arguments.
+In the above directive named ``add_jammyjam``, we call
+:meth:`~pyramid.config.Configurator.action` with two arguments: the string
+``jammyjam`` is passed as the first argument named ``discriminator``, and the
+closure function named ``register`` is passed as the second argument named
+``callable``.
+
+When the :meth:`~pyramid.config.Configurator.action` method is called, it
+appends an action to the list of pending configuration actions. All pending
+actions with the same discriminator value are potentially in conflict with
+one another (see :ref:`conflict_detection`). When the
+:meth:`~pyramid.config.Configurator.commit` method of the Configurator is
+called (either explicitly or as the result of calling
+:meth:`~pyramid.config.Configurator.make_wsgi_app`), conflicting actions are
+potentially automatically resolved as per
+:ref:`automatic_conflict_resolution`. If a conflict cannot be automatically
+resolved, a ConfigurationConflictError is raised and application startup is
+prevented.
+
+In our above example, therefore, if a consumer of our ``add_jammyjam``
+directive did this:
+
+.. code-block:: python
+ :linenos:
+
+ config.add_jammyjam('first')
+ config.add_jammyjam('second')
+
+When the action list was committed resulting from the set of calls above, our
+user's application would not start, because the discriminators of the actions
+generated by the two calls are in direct conflict. Automatic conflict
+resolution cannot resolve the conflict (because no ``config.include`` is
+involved), and the user provided no intermediate
+:meth:`pyramid.config.Configurator.commit` call between the calls to
+``add_jammyjam`` to ensure that the successive calls did not conflict with
+each other.
+
+This demonstrates the purpose of the discriminator argument to the action
+method: it's used to indicate a uniqueness constraint for an action. Two
+actions with the same discriminator will conflict unless the conflict is
+automatically or manually resolved. A discriminator can be any hashable
+object, but it is generally a string or a tuple. *You use a discriminator to
+declaratively ensure that the user doesn't provide ambiguous configuration
+statements.*
+
+But let's imagine that a consumer of ``add_jammyjam`` used it in such a way
+that no configuration conflicts are generated.
+
+.. code-block:: python
+ :linenos:
+
+ config.add_jammyjam('first')
+
+What happens now? When the ``add_jammyjam`` method is called, an action is
+appended to the pending actions list. When the pending configuration actions
+are processed during :meth:`~pyramid.config.Configurator.commit`, and no
+conflicts occur, the *callable* provided as the second argument to the
+:meth:`~pyramid.config.Configurator.action` method within ``add_jammyjam`` is
+called with no arguments. The callable in ``add_jammyjam`` is the
+``register`` closure function. It simply sets the value
+``config.registry.jammyjam`` to whatever the user passed in as the
+``jammyjam`` argument to the ``add_jammyjam`` function. Therefore, the
+result of the user's call to our directive will set the ``jammyjam``
+attribute of the registry to the string ``first``. *A callable is used by a
+directive to defer the result of a user's call to the directive until
+conflict detection has had a chance to do its job*.
+
+Other arguments exist to the :meth:`~pyramid.config.Configurator.action`
+method, including ``args``, ``kw``, ``order``, and ``introspectables``.
+
+``args`` and ``kw`` exist as values, which, if passed, will be used as
+arguments to the ``callable`` function when it is called back. For example
+our directive might use them like so:
+
+.. code-block:: python
+ :linenos:
+
+ def add_jammyjam(config, jammyjam):
+ def register(*arg, **kw):
+ config.registry.jammyjam_args = arg
+ config.registry.jammyjam_kw = kw
+ config.registry.jammyjam = jammyjam
+ config.action('jammyjam', register, args=('one',), kw={'two':'two'})
+
+In the above example, when this directive is used to generate an action, and
+that action is committed, ``config.registry.jammyjam_args`` will be set to
+``('one',)`` and ``config.registry.jammyjam_kw`` will be set to
+``{'two':'two'}``. ``args`` and ``kw`` are honestly not very useful when
+your ``callable`` is a closure function, because you already usually have
+access to every local in the directive without needing them to be passed
+back. They can be useful, however, if you don't use a closure as a callable.
+
+``order`` is a crude order control mechanism. ``order`` defaults to the
+integer ``0``; it can be set to any other integer. All actions that share an
+order will be called before other actions that share a higher order. This
+makes it possible to write a directive with callable logic that relies on the
+execution of the callable of another directive being done first. For
+example, Pyramid's :meth:`pyramid.config.Configurator.add_view` directive
+registers an action with a higher order than the
+:meth:`pyramid.config.Configurator.add_route` method. Due to this, the
+``add_view`` method's callable can assume that, if a ``route_name`` was
+passed to it, that a route by this name was already registered by
+``add_route``, and if such a route has not already been registered, it's a
+configuration error (a view that names a nonexistent route via its
+``route_name`` parameter will never be called).
+
+``introspectables`` is a sequence of :term:`introspectable` objects. You can
+pass a sequence of introspectables to the
+:meth:`~pyramid.config.Configurator.action` method, which allows you to
+augment Pyramid's configuration introspection system.
+
+.. _introspection:
+
+Configuration Introspection
+---------------------------
+
+.. warning::
+
+ The introspection subsystem is new in Pyramid 1.3.
+
+Pyramid provides a configuration introspection system that can be used by
+debugging tools to provide visibility into the configuration of a running
+application.
+
+All built-in Pyramid directives (such as
+:meth:`pyramid.config.Configurator.add_view` and
+:meth:`pyramid.config.Configurator.add_route`) register a set of
+introspectables when called. For example, when you register a view via
+``add_view``, the directive registers at least one introspectable: an
+introspectable about the view registration itself, providing human-consumable
+values for the arguments it was passed. You can later use the introspection
+query system to determine whether a particular view uses a renderer, or
+whether a particular view is limited to a particular request method, or which
+routes a particular view is registered against. The Pyramid "debug toolbar"
+makes use of the introspection system in various ways to display information
+to Pyramid developers.
+
+Introspection values are set when a sequence of :term:`introspectable`
+objects is passed to the :meth:`~pyramid.config.Configurator.action` method.
+Here's an example of a directive which uses introspectables:
+
+.. code-block:: python
+ :linenos:
+
+ def add_jammyjam(config, value):
+ def register():
+ config.registry.jammyjam = value
+ intr = config.introspectable(category_name='jammyjams',
+ discriminator='jammyjam',
+ title='a jammyjam',
+ type_name=None)
+ intr['value'] = value
+ config.action('jammyjam', register, introspectables=(intr,))
+
+ if __name__ == '__main__':
+ config = Configurator()
+ config.add_directive('add_jammyjam', add_jammyjam)
+
+If you notice, the above directive uses the ``introspectable`` attribute of a
+Configurator (:attr:`pyramid.config.Configurator.introspectable`) to create
+an introspectable object. The introspectable object's constructor requires
+at least four arguments: the ``category_name``, the ``discriminator``, the
+``title``, and the ``type_name``.
+
+The ``category_name`` is a string representing the logical category for this
+introspectable. Usually the category_name is a pluralization of the type of
+object being added via the action.
+
+The ``discriminator`` is a value unique **within the category** (unlike the
+action discriminator, which must be unique within the entire set of actions).
+It is typically a string or tuple representing the values unique to this
+introspectable within the category. It is used to generate links and as part
+of a relationship-forming target for other introspectables.
+
+The ``title`` is a human-consumable string that can be used by introspection
+system frontends to show a friendly summary of this introspectable.
+
+The ``type_name`` is a value that can be used to subtype this introspectable
+within its category for for sorting and presentation purposes. It can be any
+value.
+
+An introspectable is also dictionary-like. It can contain any set of
+key/value pairs, typically related to the arguments passed to its related
+directive. While the category_name, discriminator, title and type_name are
+*metadata* about the introspectable, the values provided as key/value pairs
+are the actual data provided by the introspectable. In the above example, we
+set the ``value`` key to the value of the ``value`` argument passed to the
+directive.
+
+Our directive above mutates the introspectable, and passes it in to the
+``action`` method as the first element of a tuple as the value of the
+``introspectable`` keyword argument. This associates this introspectable
+with the action. Introspection tools will then display this introspectable
+in their index.
+
+Introspectable Relationships
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Two introspectables may have relationships between each other.
+
+.. code-block:: python
+ :linenos:
+
+ def add_jammyjam(config, value, template):
+ def register():
+ config.registry.jammyjam = (value, template)
+ intr = config.introspectable(category_name='jammyjams',
+ discriminator='jammyjam',
+ title='a jammyjam',
+ type_name=None)
+ intr['value'] = value
+ tmpl_intr = config.introspectable(category_name='jammyjam templates',
+ discriminator=template,
+ title=template,
+ type_name=None)
+ tmpl_intr['value'] = template
+ intr.relate('jammyjam templates', template)
+ config.action('jammyjam', register, introspectables=(intr, tmpl_intr))
+
+ if __name__ == '__main__':
+ config = Configurator()
+ config.add_directive('add_jammyjam', add_jammyjam)
+
+In the above example, the ``add_jammyjam`` directive registers two
+introspectables. The first is related to the ``value`` passed to the
+directive; the second is related to the ``template`` passed to the directive.
+If you believe a concept within a directive is important enough to have its
+own introspectable, you can cause the same directive to register more than
+one introspectable, registering one introspectable for the "main idea" and
+another for a related concept.
+
+The call to ``intr.relate`` above
+(:meth:`pyramid.interfaces.IIntrospectable.relate`) is passed two arguments:
+a category name and a directive. The example above effectively indicates
+that the directive wishes to form a relationship between the ``intr``
+introspectable and the ``tmpl_intr`` introspectable; the arguments passed to
+``relate`` are the category name and discriminator of the ``tmpl_intr``
+introspectable.
+
+Relationships need not be made between two introspectables created by the
+same directive. Instead, a relationship can be formed between an
+introspectable created in one directive and another introspectable created in
+another by calling ``relate`` on either side with the other directive's
+category name and discriminator. An error will be raised at configuration
+commit time if you attempt to relate an introspectable with another
+nonexistent introspectable, however.
+
+Introspectable relationships will show up in frontend system renderings of
+introspection values. For example, if a view registration names a route
+name, the introspectable related to the view callable will show a reference
+to the route to which it relates to and vice versa.
diff --git a/docs/narr/introspector.rst b/docs/narr/introspector.rst
new file mode 100644
index 000000000..cfc6144dd
--- /dev/null
+++ b/docs/narr/introspector.rst
@@ -0,0 +1,542 @@
+.. index::
+ single: introspection
+ single: introspector
+
+.. _using_introspection:
+
+Pyramid Configuration Introspection
+===================================
+
+When Pyramid starts up, each call to a :term:`configuration directive` causes
+one or more :term:`introspectable` objects to be registered with an
+:term:`introspector`. The introspector can be queried by application code to
+obtain information about the configuration of the running application. This
+feature is useful for debug toolbars, command-line scripts which show some
+aspect of configuration, and for runtime reporting of startup-time
+configuration settings.
+
+.. warning::
+
+ Introspection is new in Pyramid 1.3.
+
+Using the Introspector
+----------------------
+
+Here's an example of using Pyramid's introspector from within a view
+callable:
+
+.. code-block:: python
+ :linenos:
+
+ from pyramid.view import view_config
+ from pyramid.response import Response
+
+ @view_config(route_name='bar')
+ def route_accepts(request):
+ introspector = request.registry.introspector
+ route_name = request.matched_route.name
+ route_intr = introspector.get('routes', route_name)
+ return Response(str(route_intr['pattern']))
+
+This view will return a response that contains the "pattern" argument
+provided to the ``add_route`` method of the route which matched when the view
+was called. It uses the :meth:`pyramid.interfaces.IIntrospector.get` method
+to return an introspectable in the category ``routes`` with a
+:term:`discriminator` equal to the matched route name. It then uses the
+returned introspectable to obtain an "pattern" value.
+
+The introspectable returned by the query methods of the introspector has
+methods and attributes described by
+:class:`pyramid.interfaces.IIntrospectable`. In particular, the
+:meth:`~pyramid.interfaces.IIntrospector.get`,
+:meth:`~pyramid.interfaces.IIntrospector.get_category`,
+:meth:`~pyramid.interfaces.IIntrospector.categories`,
+:meth:`~pyramid.interfaces.IIntrospector.categorized`, and
+:meth:`~pyramid.interfaces.IIntrospector.related` methods of an introspector
+can be used to query for introspectables.
+
+Introspectable Objects
+----------------------
+
+Introspectable objects are returned from query methods of an introspector.
+Each introspectable object implements the attributes and methods the
+documented at :class:`pyramid.interfaces.IIntrospectable`.
+
+The important attributes shared by all introspectables are the following:
+
+``title``
+
+ A human-readable text title describing the introspectable
+
+``category_name``
+
+ A text category name describing the introspection category to which this
+ introspectable belongs. It is often a plural if there are expected to be
+ more than one introspectable registered within the category.
+
+``discriminator``
+
+ A hashable object representing the unique value of this introspectable
+ within its category.
+
+``discriminator_hash``
+
+ The integer hash of the discriminator (useful for using in HTML links).
+
+``type_name``
+
+ The text name of a subtype within this introspectable's category. If there
+ is only one type name in this introspectable's category, this value will
+ often be a singular version of the category name but it can be an arbitrary
+ value.
+
+``action_info``
+
+ An object describing the directive call site which caused this
+ introspectable to be registered; contains attributes described in
+ :class:`pyramid.interfaces.IActionInfo`.
+
+Besides having the attributes described above, an introspectable is a
+dictionary-like object. An introspectable can be queried for data values via
+its ``__getitem__``, ``get``, ``keys``, ``values``, or ``items`` methods.
+For example:
+
+.. code-block:: python
+ :linenos:
+
+ route_intr = introspector.get('routes', 'edit_user')
+ pattern = route_intr['pattern']
+
+Pyramid Introspection Categories
+--------------------------------
+
+The list of concrete introspection categories provided by built-in Pyramid
+configuration directives follows. Add-on packages may supply other
+introspectables in categories not described here.
+
+``subscribers``
+
+ Each introspectable in the ``subscribers`` category represents a call to
+ :meth:`pyramid.config.Configurator.add_subscriber` (or the decorator
+ equivalent); each will have the following data.
+
+ ``subscriber``
+
+ The subscriber callable object (the resolution of the ``subscriber``
+ argument passed to ``add_susbcriber``).
+
+ ``interfaces``
+
+ A sequence of interfaces (or classes) that are subscribed to (the
+ resolution of the ``ifaces`` argument passed to ``add_subscriber``).
+
+``response adapters``
+
+ Each introspectable in the ``response adapters`` category represents a call
+ to :meth:`pyramid.config.Configurator.add_response_adapter` (or a decorator
+ equivalent); each will have the following data.
+
+ ``adapter``
+
+ The adapter object (the resolved ``adapter`` argument to
+ ``add_response_adapter``).
+
+ ``type``
+
+ The resolved ``type_or_iface`` argument passed to
+ ``add_response_adapter``.
+
+``root factories``
+
+ Each introspectable in the ``root factories`` category represents a call to
+ :meth:`pyramid.config.Configurator.set_root_factory` (or the Configurator
+ constructor equivalent) *or* a ``factory`` argument passed to
+ :meth:`pyramid.config.Configurator.add_route`; each will have the following
+ data.
+
+ ``factory``
+
+ The factory object (the resolved ``factory`` argument to
+ ``set_root_factory``).
+
+ ``route_name``
+
+ The name of the route which will use this factory. If this is the
+ *default* root factory (if it's registered during a call to
+ ``set_root_factory``), this value will be ``None``.
+
+``session factory``
+
+ Only one introspectable will exist in the ``session factory`` category. It
+ represents a call to :meth:`pyramid.config.Configurator.set_session_factory`
+ (or the Configurator constructor equivalent); it will have the following
+ data.
+
+ ``factory``
+
+ The factory object (the resolved ``factory`` argument to
+ ``set_session_factory``).
+
+``request factory``
+
+ Only one introspectable will exist in the ``request factory`` category. It
+ represents a call to :meth:`pyramid.config.Configurator.set_request_factory`
+ (or the Configurator constructor equivalent); it will have the following
+ data.
+
+ ``factory``
+
+ The factory object (the resolved ``factory`` argument to
+ ``set_request_factory``).
+
+``locale negotiator``
+
+ Only one introspectable will exist in the ``locale negotiator`` category.
+ It represents a call to
+ :meth:`pyramid.config.Configurator.set_locale_negotiator` (or the
+ Configurator constructor equivalent); it will have the following data.
+
+ ``negotiator``
+
+ The factory object (the resolved ``negotiator`` argument to
+ ``set_locale_negotiator``).
+
+``renderer factories``
+
+ Each introspectable in the ``renderer factories`` category represents a
+ call to :meth:`pyramid.config.Configurator.add_renderer` (or the
+ Configurator constructor equivalent); each will have the following data.
+
+ ``name``
+
+ The name of the renderer (the value of the ``name`` argument to
+ ``add_renderer``).
+
+ ``factory``
+
+ The factory object (the resolved ``factory`` argument to
+ ``add_renderer``).
+
+``renderer globals factory``
+
+ There will be one and only one introspectable in the ``renderer globals
+ factory`` category. It represents a call to
+ :meth:`pyramid.config.Configurator.set_renderer_globals_factory`; it will
+ have the following data.
+
+ ``factory``
+
+ The factory object (the resolved ``factory`` argument to
+ ``set_renderer_globals_factory``).
+
+``routes``
+
+ Each introspectable in the ``routes`` category represents a call to
+ :meth:`pyramid.config.Configurator.add_route`; each will have the following
+ data.
+
+ ``name``
+
+ The ``name`` argument passed to ``add_route``.
+
+ ``pattern``
+
+ The ``pattern`` argument passed to ``add_route``.
+
+ ``factory``
+
+ The (resolved) ``factory`` argument passed to ``add_route``.
+
+ ``xhr``
+
+ The ``xhr`` argument passed to ``add_route``.
+
+ ``request_method``
+
+ The ``request_method`` argument passed to ``add_route``.
+
+ ``request_methods``
+
+ A sequence of request method names implied by the ``request_method``
+ argument passed to ``add_route`` or the value ``None`` if a
+ ``request_method`` argument was not supplied.
+
+ ``path_info``
+
+ The ``path_info`` argument passed to ``add_route``.
+
+ ``request_param``
+
+ The ``request_param`` argument passed to ``add_route``.
+
+ ``header``
+
+ The ``header`` argument passed to ``add_route``.
+
+ ``accept``
+
+ The ``accept`` argument passed to ``add_route``.
+
+ ``traverse``
+
+ The ``traverse`` argument passed to ``add_route``.
+
+ ``custom_predicates``
+
+ The ``custom_predicates`` argument passed to ``add_route``.
+
+ ``pregenerator``
+
+ The ``pregenerator`` argument passed to ``add_route``.
+
+ ``pregenerator``
+
+ The ``static`` argument passed to ``add_route``.
+
+ ``pregenerator``
+
+ The ``use_global_views`` argument passed to ``add_route``.
+
+ ``object``
+
+ The :class:`pyramid.interfaces.IRoute` object that is used to perform
+ matching and generation for this route.
+
+``authentication policy``
+
+ There will be one and only one introspectable in the ``authentication
+ policy`` category. It represents a call to the
+ :meth:`pyramid.config.Configurator.set_authentication_policy` method (or
+ its Configurator constructor equivalent); it will have the following data.
+
+ ``policy``
+
+ The policy object (the resolved ``policy`` argument to
+ ``set_authentication_policy``).
+
+``authorization policy``
+
+ There will be one and only one introspectable in the ``authorization
+ policy`` category. It represents a call to the
+ :meth:`pyramid.config.Configurator.set_authorization_policy` method (or its
+ Configurator constructor equivalent); it will have the following data.
+
+ ``policy``
+
+ The policy object (the resolved ``policy`` argument to
+ ``set_authorization_policy``).
+
+``default permission``
+
+ There will be one and only one introspectable in the ``default permission``
+ category. It represents a call to the
+ :meth:`pyramid.config.Configurator.set_default_permission` method (or its
+ Configurator constructor equivalent); it will have the following data.
+
+ ``value``
+
+ The permission name passed to ``set_default_permission``.
+
+``views``
+
+ Each introspectable in the ``views`` category represents a call to
+ :meth:`pyramid.config.Configurator.add_view`; each will have the following
+ data.
+
+ ``name``
+
+ The ``name`` argument passed to ``add_view``.
+
+ ``context``
+
+ The (resolved) ``context`` argument passed to ``add_view``.
+
+ ``containment``
+
+ The (resolved) ``containment`` argument passed to ``add_view``.
+
+ ``request_param``
+
+ The ``request_param`` argument passed to ``add_view``.
+
+ ``request_methods``
+
+ A sequence of request method names implied by the ``request_method``
+ argument passed to ``add_view`` or the value ``None`` if a
+ ``request_method`` argument was not supplied.
+
+ ``route_name``
+
+ The ``route_name`` argument passed to ``add_view``.
+
+ ``attr``
+
+ The ``attr`` argument passed to ``add_view``.
+
+ ``xhr``
+
+ The ``xhr`` argument passed to ``add_view``.
+
+ ``accept``
+
+ The ``accept`` argument passed to ``add_view``.
+
+ ``header``
+
+ The ``header`` argument passed to ``add_view``.
+
+ ``path_info``
+
+ The ``path_info`` argument passed to ``add_view``.
+
+ ``match_param``
+
+ The ``match_param`` argument passed to ``add_view``.
+
+ ``callable``
+
+ The (resolved) ``view`` argument passed to ``add_view``. Represents the
+ "raw" view callable.
+
+ ``derived_callable``
+
+ The view callable derived from the ``view`` argument passed to
+ ``add_view``. Represents the view callable which Pyramid itself calls
+ (wrapped in security and other wrappers).
+
+ ``mapper``
+
+ The (resolved) ``mapper`` argument passed to ``add_view``.
+
+ ``decorator``
+
+ The (resolved) ``decorator`` argument passed to ``add_view``.
+
+``permissions``
+
+ Each introspectable in the ``permissions`` category represents a call to
+ :meth:`pyramid.config.Configurator.add_view` that has an explicit
+ ``permission`` argument to *or* a call to
+ :meth:`pyramid.config.Configurator.set_default_permission`; each will have
+ the following data.
+
+ ``value``
+
+ The permission name passed to ``add_view`` or ``set_default_permission``.
+
+``templates``
+
+ Each introspectable in the ``templates`` category represents a call to
+ :meth:`pyramid.config.Configurator.add_view` that has a ``renderer``
+ argument which points to a template; each will have the following data.
+
+ ``name``
+
+ The renderer's name (a string).
+
+ ``type``
+
+ The renderer's type (a string).
+
+ ``renderer``
+
+ The :class:`pyramid.interfaces.IRendererInfo` object which represents
+ this template's renderer.
+
+``view mapper``
+
+ Each introspectable in the ``permissions`` category represents a call to
+ :meth:`pyramid.config.Configurator.add_view` that has an explicit
+ ``mapper`` argument to *or* a call to
+ :meth:`pyramid.config.Configurator.set_view_mapper`; each will have
+ the following data.
+
+ ``mapper``
+
+ The (resolved) ``mapper`` argument passed to ``add_view`` or
+ ``set_view_mapper``.
+
+``asset overrides``
+
+ Each introspectable in the ``asset overrides`` category represents a call
+ to :meth:`pyramid.config.Configurator.override_asset`; each will have the
+ following data.
+
+ ``to_override``
+
+ The ``to_override`` argument (an asset spec) passed to
+ ``override_asset``.
+
+ ``override_with``
+
+ The ``override_with`` argument (an asset spec) passed to
+ ``override_asset``.
+
+``translation directories``
+
+ Each introspectable in the ``asset overrides`` category represents an
+ individual element in a ``specs`` argument passed to to
+ :meth:`pyramid.config.Configurator.add_translation_dirs`; each will have
+ the following data.
+
+ ``directory``
+
+ The absolute path of the translation directory.
+
+ ``spec``
+
+ The asset specification passed to ``add_translation_dirs``.
+
+``tweens``
+
+ Each introspectable in the ``tweens`` category represents a call to
+ :meth:`pyramid.config.Configurator.add_tween`; each will have the following
+ data.
+
+ ``name``
+
+ The dotted name to the tween factory as a string (passed as
+ the ``tween_factory`` argument to ``add_tween``).
+
+ ``factory``
+
+ The (resolved) tween factory object.
+
+ ``type``
+
+ ``implict`` or ``explicit`` as a string.
+
+ ``under``
+
+ The ``under`` argument passed to ``add_tween`` (a string).
+
+ ``over``
+
+ The ``over`` argument passed to ``add_tween`` (a string).
+
+Introspection in the Toolbar
+----------------------------
+
+The Pyramid debug toolbar (part of the ``pyramid_debugtoolbar`` package)
+provides a canned view of all registered introspectables and their
+relationships. It looks something like this:
+
+.. image:: tb_introspector.png
+
+Disabling Introspection
+-----------------------
+
+You can disable Pyramid introspection by passing the object
+:attr:`pyramid.registry.noop_introspector` to the :term:`Configurator`
+constructor in your application setup:
+
+.. code-block:: python
+
+ from pyramid.config import Configurator
+ from pyramid.registry import noop_introspector
+ config = Configurator(..., introspector=noop_introspector)
+
+When the noop introspector is active, all introspectables generated by
+configuration directives are thrown away. A noop introspector behaves just
+like a "real" introspector, but the methods of a noop introspector do nothing
+and return null values.
diff --git a/docs/narr/tb_introspector.png b/docs/narr/tb_introspector.png
new file mode 100644
index 000000000..231a094f7
--- /dev/null
+++ b/docs/narr/tb_introspector.png
Binary files differ
diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py
index 67269954c..315cdef07 100644
--- a/pyramid/config/__init__.py
+++ b/pyramid/config/__init__.py
@@ -1,5 +1,6 @@
import inspect
import logging
+import operator
import os
import sys
import types
@@ -39,7 +40,11 @@ from pyramid.path import (
package_of,
)
-from pyramid.registry import Registry
+from pyramid.registry import (
+ Introspectable,
+ Introspector,
+ Registry,
+ )
from pyramid.router import Router
@@ -50,6 +55,7 @@ from pyramid.threadlocal import manager
from pyramid.util import (
DottedNameResolver,
WeakOrderedSet,
+ object_description,
)
from pyramid.config.adapters import AdaptersConfiguratorMixin
@@ -63,11 +69,15 @@ from pyramid.config.security import SecurityConfiguratorMixin
from pyramid.config.settings import SettingsConfiguratorMixin
from pyramid.config.testing import TestingConfiguratorMixin
from pyramid.config.tweens import TweensConfiguratorMixin
-from pyramid.config.util import action_method
+from pyramid.config.util import (
+ action_method,
+ ActionInfo,
+ )
from pyramid.config.views import ViewsConfiguratorMixin
from pyramid.config.zca import ZCAConfiguratorMixin
empty = text_('')
+_marker = object()
ConfigurationError = ConfigurationError # pyflakes
@@ -226,14 +236,22 @@ class Configurator(
If ``route_prefix`` is passed, all routes added with
:meth:`pyramid.config.Configurator.add_route` will have the specified path
- prepended to their pattern. This parameter is new in Pyramid 1.2."""
+ prepended to their pattern. This parameter is new in Pyramid 1.2.
+ If ``introspector`` is passed, it must be an instance implementing the
+ attributes and methods of :class:`pyramid.interfaces.IIntrospector`. If
+ ``introspector`` is not passed (or is passed as ``None``), the default
+ introspector implementation will be used. This parameter is new in
+ Pyramid 1.3.
+ """
manager = manager # for testing injection
venusian = venusian # for testing injection
_ainfo = None
basepath = None
includepath = ()
info = ''
+ object_description = staticmethod(object_description)
+ introspectable = Introspectable
def __init__(self,
registry=None,
@@ -253,6 +271,7 @@ class Configurator(
autocommit=False,
exceptionresponse_view=default_exceptionresponse_view,
route_prefix=None,
+ introspector=None,
):
if package is None:
package = caller_package()
@@ -280,15 +299,24 @@ class Configurator(
session_factory=session_factory,
default_view_mapper=default_view_mapper,
exceptionresponse_view=exceptionresponse_view,
+ introspector=introspector,
)
- def setup_registry(self, settings=None, root_factory=None,
- authentication_policy=None, authorization_policy=None,
- renderers=None, debug_logger=None,
- locale_negotiator=None, request_factory=None,
- renderer_globals_factory=None, default_permission=None,
- session_factory=None, default_view_mapper=None,
- exceptionresponse_view=default_exceptionresponse_view):
+ def setup_registry(self,
+ settings=None,
+ root_factory=None,
+ authentication_policy=None,
+ authorization_policy=None,
+ renderers=None,
+ debug_logger=None,
+ locale_negotiator=None,
+ request_factory=None,
+ renderer_globals_factory=None,
+ default_permission=None,
+ session_factory=None,
+ default_view_mapper=None,
+ exceptionresponse_view=default_exceptionresponse_view,
+ introspector=None):
""" When you pass a non-``None`` ``registry`` argument to the
:term:`Configurator` constructor, no initial setup is performed
against the registry. This is because the registry you pass in may
@@ -308,6 +336,10 @@ class Configurator(
registry = self.registry
self._fix_registry()
+
+ if introspector is not None:
+ self.introspector = introspector
+
self._set_settings(settings)
self._register_response_adapters()
@@ -437,8 +469,37 @@ class Configurator(
_registry.registerSelfAdapter = registerSelfAdapter
# API
+
+ def _get_introspector(self):
+ introspector = getattr(self.registry, 'introspector', _marker)
+ if introspector is _marker:
+ introspector = Introspector()
+ self._set_introspector(introspector)
+ return introspector
+
+ def _set_introspector(self, introspector):
+ self.registry.introspector = introspector
+
+ def _del_introspector(self):
+ del self.registry.introspector
+
+ introspector = property(_get_introspector,
+ _set_introspector,
+ _del_introspector)
+
+ @property
+ def action_info(self):
+ info = self.info # usually a ZCML action (ParserInfo) if self.info
+ if not info:
+ # Try to provide more accurate info for conflict reports
+ if self._ainfo:
+ info = self._ainfo[0]
+ else:
+ info = ActionInfo(None, 0, '', '')
+ return info
- def action(self, discriminator, callable=None, args=(), kw=None, order=0):
+ def action(self, discriminator, callable=None, args=(), kw=None, order=0,
+ introspectables=(), **extra):
""" Register an action which will be executed when
:meth:`pyramid.config.Configurator.commit` is called (or executed
immediately if ``autocommit`` is ``True``).
@@ -451,40 +512,54 @@ class Configurator(
given, but it can be ``None``, to indicate that the action never
conflicts. It must be a hashable value.
- The ``callable`` is a callable object which performs the action. It
- is optional. ``args`` and ``kw`` are tuple and dict objects
- respectively, which are passed to ``callable`` when this action is
- executed.
+ The ``callable`` is a callable object which performs the task
+ associated with the action when the action is executed. It is
+ optional.
+
+ ``args`` and ``kw`` are tuple and dict objects respectively, which
+ are passed to ``callable`` when this action is executed. Both are
+ optional.
+
+ ``order`` is a grouping mechanism; an action with a lower order will
+ be executed before an action with a higher order (has no effect when
+ autocommit is ``True``).
- ``order`` is a crude order control mechanism, only rarely used (has
- no effect when autocommit is ``True``).
+ ``introspectables`` is a sequence of :term:`introspectable` objects
+ (or the empty sequence if no introspectable objects are associated
+ with this action).
+
+ ``extra`` provides a facility for inserting extra keys and values
+ into an action dictionary.
"""
if kw is None:
kw = {}
autocommit = self.autocommit
+ action_info = self.action_info
+ introspector = self.introspector
if autocommit:
if callable is not None:
callable(*args, **kw)
+ if introspector is not None:
+ for introspectable in introspectables:
+ introspectable.register(introspector, action_info)
else:
- info = self.info # usually a ZCML action if self.info has data
- if not info:
- # Try to provide more accurate info for conflict reports
- if self._ainfo:
- info = self._ainfo[0]
- else:
- info = ''
- self.action_state.action(
- discriminator,
- callable,
- args,
- kw,
- order,
- info=info,
- includepath=self.includepath,
+ action = extra
+ action.update(
+ dict(
+ discriminator=discriminator,
+ callable=callable,
+ args=args,
+ kw=kw,
+ order=order,
+ info=action_info,
+ includepath=self.includepath,
+ introspectables=introspectables,
+ )
)
+ self.action_state.action(**action)
def _get_action_state(self):
registry = self.registry
@@ -509,7 +584,7 @@ class Configurator(
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."""
- self.action_state.execute_actions()
+ self.action_state.execute_actions(introspector=self.introspector)
self.action_state = ActionState() # old actions have been processed
def include(self, callable, route_prefix=None):
@@ -862,22 +937,27 @@ class ActionState(object):
return True
def action(self, discriminator, callable=None, args=(), kw=None, order=0,
- includepath=(), info=''):
+ includepath=(), info=None, introspectables=(), **extra):
"""Add an action with the given discriminator, callable and arguments
"""
- # NB: note that the ordering and composition of the action tuple should
- # not change without first ensuring that ``pyramid_zcml`` appends
- # similarly-composed actions to our .actions variable (as silly as
- # the composition and ordering is).
if kw is None:
kw = {}
- action = (discriminator, callable, args, kw, includepath, info, order)
- # remove trailing false items
- while (len(action) > 2) and not action[-1]:
- action = action[:-1]
+ action = extra
+ action.update(
+ dict(
+ discriminator=discriminator,
+ callable=callable,
+ args=args,
+ kw=kw,
+ includepath=includepath,
+ info=info,
+ order=order,
+ introspectables=introspectables,
+ )
+ )
self.actions.append(action)
- def execute_actions(self, clear=True):
+ def execute_actions(self, clear=True, introspector=None):
"""Execute the configuration actions
This calls the action callables after resolving conflicts
@@ -918,21 +998,26 @@ class ActionState(object):
in:
oops
-
Note that actions executed before the error still have an effect:
>>> output
[('f', (1,), {}), ('f', (2,), {})]
-
"""
+
try:
for action in resolveConflicts(self.actions):
- _, callable, args, kw, _, info, _ = expand_action(*action)
- if callable is None:
- continue
+ callable = action['callable']
+ args = action['args']
+ kw = action['kw']
+ info = action['info']
+ # we use "get" below in case an action was added via a ZCML
+ # directive that did not know about introspectables
+ introspectables = action.get('introspectables', ())
+
try:
- callable(*args, **kw)
+ if callable is not None:
+ callable(*args, **kw)
except (KeyboardInterrupt, SystemExit): # pragma: no cover
raise
except:
@@ -943,6 +1028,11 @@ class ActionState(object):
tb)
finally:
del t, v, tb
+
+ if introspector is not None:
+ for introspectable in introspectables:
+ introspectable.register(introspector, info)
+
finally:
if clear:
del self.actions[:]
@@ -952,120 +1042,95 @@ def resolveConflicts(actions):
"""Resolve conflicting actions
Given an actions list, identify and try to resolve conflicting actions.
- Actions conflict if they have the same non-null discriminator.
+ Actions conflict if they have the same non-None discriminator.
Conflicting actions can be resolved if the include path of one of
the actions is a prefix of the includepaths of the other
conflicting actions and is unequal to the include paths in the
other conflicting actions.
-
- Here are some examples to illustrate how this works:
-
- >>> from zope.configmachine.tests.directives import f
- >>> from pprint import PrettyPrinter
- >>> pprint=PrettyPrinter(width=60).pprint
- >>> pprint(resolveConflicts([
- ... (None, f),
- ... (1, f, (1,), {}, (), 'first'),
- ... (1, f, (2,), {}, ('x',), 'second'),
- ... (1, f, (3,), {}, ('y',), 'third'),
- ... (4, f, (4,), {}, ('y',), 'should be last', 99999),
- ... (3, f, (3,), {}, ('y',)),
- ... (None, f, (5,), {}, ('y',)),
- ... ]))
- [(None, f),
- (1, f, (1,), {}, (), 'first'),
- (3, f, (3,), {}, ('y',)),
- (None, f, (5,), {}, ('y',)),
- (4, f, (4,), {}, ('y',), 'should be last')]
-
- >>> try:
- ... v = resolveConflicts([
- ... (None, f),
- ... (1, f, (2,), {}, ('x',), 'eek'),
- ... (1, f, (3,), {}, ('y',), 'ack'),
- ... (4, f, (4,), {}, ('y',)),
- ... (3, f, (3,), {}, ('y',)),
- ... (None, f, (5,), {}, ('y',)),
- ... ])
- ... except ConfigurationConflictError, v:
- ... pass
- >>> print v
- Conflicting configuration actions
- For: 1
- eek
- ack
-
"""
# organize actions by discriminators
unique = {}
output = []
- for i in range(len(actions)):
- (discriminator, callable, args, kw, includepath, info, order
- ) = expand_action(*(actions[i]))
+ for i, action in enumerate(actions):
+ if not isinstance(action, dict):
+ # old-style tuple action
+ action = expand_action(*action)
+
+ # "order" is an integer grouping. Actions in a lower order will be
+ # executed before actions in a higher order. Within an order,
+ # actions are executed sequentially based on original action ordering
+ # ("i").
+ order = action['order'] or 0
+ discriminator = action['discriminator']
+
+ # "ainfo" is a tuple of (order, i, action) where "order" is a
+ # user-supplied grouping, "i" is an integer expressing the relative
+ # position of this action in the action list being resolved, and
+ # "action" is an action dictionary. The purpose of an ainfo is to
+ # associate an "order" and an "i" with a particular action; "order"
+ # and "i" exist for sorting purposes after conflict resolution.
+ ainfo = (order, i, action)
- order = order or i
if discriminator is None:
- # The discriminator is None, so this directive can
- # never conflict. We can add it directly to the
- # configuration actions.
- output.append(
- (order, discriminator, callable, args, kw, includepath, info)
- )
+ # The discriminator is None, so this action can never conflict.
+ # We can add it directly to the result.
+ output.append(ainfo)
continue
-
- a = unique.setdefault(discriminator, [])
- a.append(
- (includepath, order, callable, args, kw, info)
- )
+ L = unique.setdefault(discriminator, [])
+ L.append(ainfo)
# Check for conflicts
conflicts = {}
- for discriminator, dups in unique.items():
-
- # We need to sort the actions by the paths so that the shortest
- # path with a given prefix comes first:
- def allbutfunc(stupid):
- # f me with a shovel, py3 cant cope with sorting when the
- # callable function is in the list
- return stupid[0:2] + stupid[3:]
- dups.sort(key=allbutfunc)
- (basepath, i, callable, args, kw, baseinfo) = dups[0]
- output.append(
- (i, discriminator, callable, args, kw, basepath, baseinfo)
- )
- for includepath, i, callable, args, kw, info in dups[1:]:
+
+ for discriminator, ainfos in unique.items():
+
+ # We use (includepath, order, i) as a sort key because we need to
+ # sort the actions by the paths so that the shortest path with a
+ # given prefix comes first. The "first" action is the one with the
+ # shortest include path. We break sorting ties using "order", then
+ # "i".
+ def bypath(ainfo):
+ path, order, i = ainfo[2]['includepath'], ainfo[0], ainfo[1]
+ return path, order, i
+
+ ainfos.sort(key=bypath)
+ ainfo, rest = ainfos[0], ainfos[1:]
+ output.append(ainfo)
+ _, _, action = ainfo
+ basepath, baseinfo, discriminator = (action['includepath'],
+ action['info'],
+ action['discriminator'])
+
+ for _, _, action in rest:
+ includepath = action['includepath']
# Test whether path is a prefix of opath
if (includepath[:len(basepath)] != basepath # not a prefix
- or
- (includepath == basepath)
- ):
- if discriminator not in conflicts:
- conflicts[discriminator] = [baseinfo]
- conflicts[discriminator].append(info)
-
+ or includepath == basepath):
+ L = conflicts.setdefault(discriminator, [baseinfo])
+ L.append(action['info'])
if conflicts:
raise ConfigurationConflictError(conflicts)
- # Now put the output back in the original order, and return it:
- output.sort()
- r = []
- for o in output:
- action = o[1:]
- while len(action) > 2 and not action[-1]:
- action = action[:-1]
- r.append(action)
-
- return r
-
-# this function is licensed under the ZPL (stolen from Zope)
+ # sort conflict-resolved actions by (order, i) and return them
+ return [ x[2] for x in sorted(output, key=operator.itemgetter(0, 1))]
+
def expand_action(discriminator, callable=None, args=(), kw=None,
- includepath=(), info='', order=0):
+ includepath=(), info=None, order=0, introspectables=()):
if kw is None:
kw = {}
- return (discriminator, callable, args, kw, includepath, info, order)
+ return dict(
+ discriminator=discriminator,
+ callable=callable,
+ args=args,
+ kw=kw,
+ includepath=includepath,
+ info=info,
+ order=order,
+ introspectables=introspectables,
+ )
global_registries = WeakOrderedSet()
diff --git a/pyramid/config/adapters.py b/pyramid/config/adapters.py
index f022e7f08..04571bec3 100644
--- a/pyramid/config/adapters.py
+++ b/pyramid/config/adapters.py
@@ -27,7 +27,13 @@ class AdaptersConfiguratorMixin(object):
iface = (iface,)
def register():
self.registry.registerHandler(subscriber, iface)
- self.action(None, register)
+ intr = self.introspectable('subscribers',
+ id(subscriber),
+ self.object_description(subscriber),
+ 'subscriber')
+ intr['subscriber'] = subscriber
+ intr['interfaces'] = iface
+ self.action(None, register, introspectables=(intr,))
return subscriber
@action_method
@@ -52,7 +58,15 @@ class AdaptersConfiguratorMixin(object):
reg.registerSelfAdapter((type_or_iface,), IResponse)
else:
reg.registerAdapter(adapter, (type_or_iface,), IResponse)
- self.action((IResponse, type_or_iface), register)
+ discriminator = (IResponse, type_or_iface)
+ intr = self.introspectable(
+ 'response adapters',
+ discriminator,
+ self.object_description(adapter),
+ 'response adapter')
+ intr['adapter'] = adapter
+ intr['type'] = type_or_iface
+ self.action(discriminator, register, introspectables=(intr,))
def _register_response_adapters(self):
# cope with WebOb response objects that aren't decorated with IResponse
diff --git a/pyramid/config/assets.py b/pyramid/config/assets.py
index 08cc6dc38..c93431987 100644
--- a/pyramid/config/assets.py
+++ b/pyramid/config/assets.py
@@ -236,7 +236,15 @@ class AssetsConfiguratorMixin(object):
to_package = sys.modules[override_package]
override(from_package, path, to_package, override_prefix)
- self.action(None, register)
+ intr = self.introspectable(
+ 'asset overrides',
+ (package, override_package, path, override_prefix),
+ '%s -> %s' % (to_override, override_with),
+ 'asset override',
+ )
+ intr['to_override'] = to_override
+ intr['override_with'] = override_with
+ self.action(None, register, introspectables=(intr,))
override_resource = override_asset # bw compat
diff --git a/pyramid/config/factories.py b/pyramid/config/factories.py
index a5a797a47..530b6cc28 100644
--- a/pyramid/config/factories.py
+++ b/pyramid/config/factories.py
@@ -28,15 +28,21 @@ class FactoriesConfiguratorMixin(object):
def register():
self.registry.registerUtility(factory, IRootFactory)
self.registry.registerUtility(factory, IDefaultRootFactory) # b/c
- self.action(IRootFactory, register)
+
+ intr = self.introspectable('root factories',
+ None,
+ self.object_description(factory),
+ 'root factory')
+ intr['factory'] = factory
+ self.action(IRootFactory, register, introspectables=(intr,))
_set_root_factory = set_root_factory # bw compat
@action_method
- def set_session_factory(self, session_factory):
+ def set_session_factory(self, factory):
"""
Configure the application with a :term:`session factory`. If this
- method is called, the ``session_factory`` argument must be a session
+ method is called, the ``factory`` argument must be a session
factory callable or a :term:`dotted Python name` to that factory.
.. note::
@@ -45,10 +51,14 @@ class FactoriesConfiguratorMixin(object):
:class:`pyramid.config.Configurator` constructor can be used to
achieve the same purpose.
"""
- session_factory = self.maybe_dotted(session_factory)
+ factory = self.maybe_dotted(factory)
def register():
- self.registry.registerUtility(session_factory, ISessionFactory)
- self.action(ISessionFactory, register)
+ self.registry.registerUtility(factory, ISessionFactory)
+ intr = self.introspectable('session factory', None,
+ self.object_description(factory),
+ 'session factory')
+ intr['factory'] = factory
+ self.action(ISessionFactory, register, introspectables=(intr,))
@action_method
def set_request_factory(self, factory):
@@ -69,5 +79,9 @@ class FactoriesConfiguratorMixin(object):
factory = self.maybe_dotted(factory)
def register():
self.registry.registerUtility(factory, IRequestFactory)
- self.action(IRequestFactory, register)
+ intr = self.introspectable('request factory', None,
+ self.object_description(factory),
+ 'request factory')
+ intr['factory'] = factory
+ self.action(IRequestFactory, register, introspectables=(intr,))
diff --git a/pyramid/config/i18n.py b/pyramid/config/i18n.py
index 34df1bb47..67a7e2018 100644
--- a/pyramid/config/i18n.py
+++ b/pyramid/config/i18n.py
@@ -40,12 +40,17 @@ class I18NConfiguratorMixin(object):
"""
def register():
self._set_locale_negotiator(negotiator)
- self.action(ILocaleNegotiator, register)
+ intr = self.introspectable('locale negotiator', None,
+ self.object_description(negotiator),
+ 'locale negotiator')
+ intr['negotiator'] = negotiator
+ self.action(ILocaleNegotiator, register, introspectables=(intr,))
def _set_locale_negotiator(self, negotiator):
locale_negotiator = self.maybe_dotted(negotiator)
self.registry.registerUtility(locale_negotiator, ILocaleNegotiator)
+ @action_method
def add_translation_dirs(self, *specs):
""" Add one or more :term:`translation directory` paths to the
current configuration state. The ``specs`` argument is a
@@ -71,8 +76,10 @@ class I18NConfiguratorMixin(object):
in the order they're provided in the ``*specs`` list argument (items
earlier in the list trump ones later in the list).
"""
- for spec in specs[::-1]: # reversed
+ directories = []
+ introspectables = []
+ for spec in specs[::-1]: # reversed
package_name, filename = self._split_spec(spec)
if package_name is None: # absolute filename
directory = filename
@@ -82,25 +89,35 @@ class I18NConfiguratorMixin(object):
directory = os.path.join(package_path(package), filename)
if not os.path.isdir(os.path.realpath(directory)):
- raise ConfigurationError('"%s" is not a directory' % directory)
+ raise ConfigurationError('"%s" is not a directory' %
+ directory)
+ intr = self.introspectable('translation directories', directory,
+ spec, 'translation directory')
+ intr['directory'] = directory
+ intr['spec'] = spec
+ introspectables.append(intr)
+ directories.append(directory)
- tdirs = self.registry.queryUtility(ITranslationDirectories)
- if tdirs is None:
- tdirs = []
- self.registry.registerUtility(tdirs, ITranslationDirectories)
+ def register():
+ for directory in directories:
- tdirs.insert(0, directory)
- # XXX no action?
+ tdirs = self.registry.queryUtility(ITranslationDirectories)
+ if tdirs is None:
+ tdirs = []
+ self.registry.registerUtility(tdirs,
+ ITranslationDirectories)
- if specs:
+ tdirs.insert(0, directory)
- # We actually only need an IChameleonTranslate function
- # utility to be registered zero or one times. We register the
- # same function once for each added translation directory,
- # which does too much work, but has the same effect.
+ if directories:
+ # We actually only need an IChameleonTranslate function
+ # utility to be registered zero or one times. We register the
+ # same function once for each added translation directory,
+ # which does too much work, but has the same effect.
+ ctranslate = ChameleonTranslate(translator)
+ self.registry.registerUtility(ctranslate, IChameleonTranslate)
- ctranslate = ChameleonTranslate(translator)
- self.registry.registerUtility(ctranslate, IChameleonTranslate)
+ self.action(None, register, introspectables=introspectables)
def translator(msg):
request = get_current_request()
diff --git a/pyramid/config/rendering.py b/pyramid/config/rendering.py
index a18a9b196..926511b7b 100644
--- a/pyramid/config/rendering.py
+++ b/pyramid/config/rendering.py
@@ -48,9 +48,16 @@ class RenderingConfiguratorMixin(object):
name = ''
def register():
self.registry.registerUtility(factory, IRendererFactory, name=name)
+ intr = self.introspectable('renderer factories',
+ name,
+ self.object_description(factory),
+ 'renderer factory')
+ intr['factory'] = factory
+ intr['name'] = name
# we need to register renderers early (in phase 1) because they are
# used during view configuration (which happens in phase 3)
- self.action((IRendererFactory, name), register, order=PHASE1_CONFIG)
+ self.action((IRendererFactory, name), register, order=PHASE1_CONFIG,
+ introspectables=(intr,))
@action_method
def set_renderer_globals_factory(self, factory, warn=True):
@@ -68,7 +75,9 @@ class RenderingConfiguratorMixin(object):
.. warning::
- This method is deprecated as of Pyramid 1.1.
+ This method is deprecated as of Pyramid 1.1. Use a BeforeRender
+ event subscriber as documented in the :ref:`hooks_chapter` chapter
+ instead.
.. note::
@@ -88,4 +97,8 @@ class RenderingConfiguratorMixin(object):
factory = self.maybe_dotted(factory)
def register():
self.registry.registerUtility(factory, IRendererGlobalsFactory)
+ intr = self.introspectable('renderer globals factory', None,
+ self.object_description(factory),
+ 'renderer globals factory')
+ intr['factory'] = factory
self.action(IRendererGlobalsFactory, register)
diff --git a/pyramid/config/routes.py b/pyramid/config/routes.py
index e190e56ee..ea39b6805 100644
--- a/pyramid/config/routes.py
+++ b/pyramid/config/routes.py
@@ -14,6 +14,7 @@ from pyramid.urldispatch import RoutesMapper
from pyramid.config.util import (
action_method,
make_predicates,
+ as_sorted_tuple,
)
class RoutesConfiguratorMixin(object):
@@ -347,6 +348,9 @@ class RoutesConfiguratorMixin(object):
"""
# these are route predicates; if they do not match, the next route
# in the routelist will be tried
+ if request_method is not None:
+ request_method = as_sorted_tuple(request_method)
+
ignored, predicates, ignored = make_predicates(
xhr=xhr,
request_method=request_method,
@@ -369,6 +373,38 @@ class RoutesConfiguratorMixin(object):
mapper = self.get_routes_mapper()
+ introspectables = []
+
+ intr = self.introspectable('routes',
+ name,
+ '%s (pattern: %r)' % (name, pattern),
+ 'route')
+ intr['name'] = name
+ intr['pattern'] = pattern
+ intr['factory'] = factory
+ intr['xhr'] = xhr
+ intr['request_methods'] = request_method
+ intr['path_info'] = path_info
+ intr['request_param'] = request_param
+ intr['header'] = header
+ intr['accept'] = accept
+ intr['traverse'] = traverse
+ intr['custom_predicates'] = custom_predicates
+ intr['pregenerator'] = pregenerator
+ intr['static'] = static
+ intr['use_global_views'] = use_global_views
+ introspectables.append(intr)
+
+ if factory:
+ factory_intr = self.introspectable('root factories',
+ name,
+ self.object_description(factory),
+ 'root factory')
+ factory_intr['factory'] = factory
+ factory_intr['route_name'] = name
+ factory_intr.relate('routes', name)
+ introspectables.append(factory_intr)
+
def register_route_request_iface():
request_iface = self.registry.queryUtility(IRouteRequest, name=name)
if request_iface is None:
@@ -381,9 +417,12 @@ class RoutesConfiguratorMixin(object):
request_iface, IRouteRequest, name=name)
def register_connect():
- return mapper.connect(name, pattern, factory, predicates=predicates,
- pregenerator=pregenerator, static=static)
-
+ route = mapper.connect(
+ name, pattern, factory, predicates=predicates,
+ pregenerator=pregenerator, static=static
+ )
+ intr['object'] = route
+ return route
# We have to connect routes in the order they were provided;
# we can't use a phase to do that, because when the actions are
@@ -393,7 +432,7 @@ class RoutesConfiguratorMixin(object):
# But IRouteRequest interfaces must be registered before we begin to
# process view registrations (in phase 3)
self.action(('route', name), register_route_request_iface,
- order=PHASE2_CONFIG)
+ order=PHASE2_CONFIG, introspectables=introspectables)
# deprecated adding views from add_route; must come after
# route registration for purposes of autocommit ordering
diff --git a/pyramid/config/security.py b/pyramid/config/security.py
index 23cd5f27f..a0ea173ba 100644
--- a/pyramid/config/security.py
+++ b/pyramid/config/security.py
@@ -31,8 +31,13 @@ class SecurityConfiguratorMixin(object):
'Cannot configure an authentication policy without '
'also configuring an authorization policy '
'(use the set_authorization_policy method)')
+ intr = self.introspectable('authentication policy', None,
+ self.object_description(policy),
+ 'authentication policy')
+ intr['policy'] = policy
# authentication policy used by view config (phase 3)
- self.action(IAuthenticationPolicy, register, order=PHASE2_CONFIG)
+ self.action(IAuthenticationPolicy, register, order=PHASE2_CONFIG,
+ introspectables=(intr,))
def _set_authentication_policy(self, policy):
policy = self.maybe_dotted(policy)
@@ -62,6 +67,10 @@ class SecurityConfiguratorMixin(object):
'also configuring an authentication policy '
'(use the set_authorization_policy method)')
+ intr = self.introspectable('authorization policy', None,
+ self.object_description(policy),
+ 'authorization policy')
+ intr['policy'] = policy
# authorization policy used by view config (phase 3) and
# authentication policy (phase 2)
self.action(IAuthorizationPolicy, register, order=PHASE1_CONFIG)
@@ -110,9 +119,20 @@ class SecurityConfiguratorMixin(object):
:class:`pyramid.config.Configurator` constructor can be used to
achieve the same purpose.
"""
- # default permission used during view registration (phase 3)
def register():
self.registry.registerUtility(permission, IDefaultPermission)
- self.action(IDefaultPermission, register, order=PHASE1_CONFIG)
+ intr = self.introspectable('default permission',
+ None,
+ permission,
+ 'default permission')
+ intr['value'] = permission
+ perm_intr = self.introspectable('permissions',
+ permission,
+ permission,
+ 'permission')
+ perm_intr['value'] = permission
+ # default permission used during view registration (phase 3)
+ self.action(IDefaultPermission, register, order=PHASE1_CONFIG,
+ introspectables=(intr, perm_intr,))
diff --git a/pyramid/config/tweens.py b/pyramid/config/tweens.py
index 76efe4af5..1a83f0de9 100644
--- a/pyramid/config/tweens.py
+++ b/pyramid/config/tweens.py
@@ -138,11 +138,22 @@ class TweensConfiguratorMixin(object):
raise ConfigurationError('%s cannot be under MAIN' % name)
registry = self.registry
+ introspectables = []
tweens = registry.queryUtility(ITweens)
if tweens is None:
tweens = Tweens()
registry.registerUtility(tweens, ITweens)
+ ex_intr = self.introspectable('tweens',
+ ('tween', EXCVIEW, False),
+ EXCVIEW,
+ 'implicit tween')
+ ex_intr['name'] = EXCVIEW
+ ex_intr['factory'] = excview_tween_factory
+ ex_intr['type'] = 'implicit'
+ ex_intr['under'] = None
+ ex_intr['over'] = MAIN
+ introspectables.append(ex_intr)
tweens.add_implicit(EXCVIEW, excview_tween_factory, over=MAIN)
def register():
@@ -151,7 +162,20 @@ class TweensConfiguratorMixin(object):
else:
tweens.add_implicit(name, tween_factory, under=under, over=over)
- self.action(('tween', name, explicit), register)
+ discriminator = ('tween', name, explicit)
+ tween_type = explicit and 'explicit' or 'implicit'
+
+ intr = self.introspectable('tweens',
+ discriminator,
+ name,
+ '%s tween' % tween_type)
+ intr['name'] = name
+ intr['factory'] = tween_factory
+ intr['type'] = tween_type
+ intr['under'] = under
+ intr['over'] = over
+ introspectables.append(intr)
+ self.action(discriminator, register, introspectables=introspectables)
class CyclicDependencyError(Exception):
def __init__(self, cycles):
@@ -191,7 +215,7 @@ class Tweens(object):
self.order += [(u, name) for u in under]
self.req_under.add(name)
if over is not None:
- if not is_nonstr_iter(over): #hasattr(over, '__iter__'):
+ if not is_nonstr_iter(over):
over = (over,)
self.order += [(name, o) for o in over]
self.req_over.add(name)
diff --git a/pyramid/config/util.py b/pyramid/config/util.py
index cbec2e0c2..3a2f911dc 100644
--- a/pyramid/config/util.py
+++ b/pyramid/config/util.py
@@ -1,6 +1,10 @@
import re
import traceback
+from zope.interface import implementer
+
+from pyramid.interfaces import IActionInfo
+
from pyramid.compat import (
string_types,
bytes_,
@@ -19,6 +23,19 @@ from hashlib import md5
MAX_ORDER = 1 << 30
DEFAULT_PHASH = md5().hexdigest()
+@implementer(IActionInfo)
+class ActionInfo(object):
+ def __init__(self, file, line, function, src):
+ self.file = file
+ self.line = line
+ self.function = function
+ self.src = src
+
+ def __str__(self):
+ srclines = self.src.split('\n')
+ src = '\n'.join(' %s' % x for x in srclines)
+ return 'Line %s of file %s:\n%s' % (self.line, self.file, src)
+
def action_method(wrapped):
""" Wrapper to provide the right conflict info report data when a method
that calls Configurator.action calls another that does the same"""
@@ -26,12 +43,15 @@ def action_method(wrapped):
if self._ainfo is None:
self._ainfo = []
info = kw.pop('_info', None)
+ if is_nonstr_iter(info) and len(info) == 4:
+ # _info permitted as extract_stack tuple
+ info = ActionInfo(*info)
if info is None:
try:
f = traceback.extract_stack(limit=3)
- info = f[-2]
+ info = ActionInfo(*f[-2])
except: # pragma: no cover
- info = ''
+ info = ActionInfo(None, 0, '', '')
self._ainfo.append(info)
try:
result = wrapped(self, *arg, **kw)
diff --git a/pyramid/config/views.py b/pyramid/config/views.py
index 2a6157ffb..0b6c6070f 100644
--- a/pyramid/config/views.py
+++ b/pyramid/config/views.py
@@ -941,6 +941,42 @@ class ViewsConfiguratorMixin(object):
name=renderer, package=self.package,
registry = self.registry)
+ introspectables = []
+ discriminator = [
+ 'view', context, name, request_type, IView, containment,
+ request_param, request_method, route_name, attr,
+ xhr, accept, header, path_info, match_param]
+ discriminator.extend(sorted([hash(x) for x in custom_predicates]))
+ discriminator = tuple(discriminator)
+ if inspect.isclass(view) and attr:
+ view_desc = 'method %r of %s' % (
+ attr, self.object_description(view))
+ else:
+ view_desc = self.object_description(view)
+ view_intr = self.introspectable('views',
+ discriminator,
+ view_desc,
+ 'view')
+ view_intr.update(
+ dict(name=name,
+ context=context,
+ containment=containment,
+ request_param=request_param,
+ request_methods=request_method,
+ route_name=route_name,
+ attr=attr,
+ xhr=xhr,
+ accept=accept,
+ header=header,
+ path_info=path_info,
+ match_param=match_param,
+ callable=view,
+ mapper=mapper,
+ decorator=decorator,
+ )
+ )
+ introspectables.append(view_intr)
+
def register(permission=permission, renderer=renderer):
request_iface = IRequest
if route_name is not None:
@@ -982,6 +1018,7 @@ class ViewsConfiguratorMixin(object):
decorator=decorator,
http_cache=http_cache)
derived_view = deriver(view)
+ view_intr['derived_callable'] = derived_view
registered = self.registry.adapters.registered
@@ -1079,13 +1116,33 @@ class ViewsConfiguratorMixin(object):
(IExceptionViewClassifier, request_iface, context),
IMultiView, name=name)
- discriminator = [
- 'view', context, name, request_type, IView, containment,
- request_param, request_method, route_name, attr,
- xhr, accept, header, path_info, match_param]
- discriminator.extend(sorted([hash(x) for x in custom_predicates]))
- discriminator = tuple(discriminator)
- self.action(discriminator, register)
+ if mapper:
+ mapper_intr = self.introspectable('view mappers',
+ discriminator,
+ 'view mapper for %s' % view_desc,
+ 'view mapper')
+ mapper_intr['mapper'] = mapper
+ mapper_intr.relate('views', discriminator)
+ introspectables.append(mapper_intr)
+ if route_name:
+ view_intr.relate('routes', route_name) # see add_route
+ if renderer is not None and renderer.name and '.' in renderer.name:
+ # it's a template
+ tmpl_intr = self.introspectable('templates', discriminator,
+ renderer.name, 'template')
+ tmpl_intr.relate('views', discriminator)
+ tmpl_intr['name'] = renderer.name
+ tmpl_intr['type'] = renderer.type
+ tmpl_intr['renderer'] = renderer
+ tmpl_intr.relate('renderer factories', renderer.type)
+ introspectables.append(tmpl_intr)
+ if permission is not None:
+ perm_intr = self.introspectable('permissions', permission,
+ permission, 'permission')
+ perm_intr['value'] = permission
+ perm_intr.relate('views', discriminator)
+ introspectables.append(perm_intr)
+ self.action(discriminator, register, introspectables=introspectables)
def derive_view(self, view, attr=None, renderer=None):
"""
@@ -1312,7 +1369,13 @@ class ViewsConfiguratorMixin(object):
self.registry.registerUtility(mapper, IViewMapperFactory)
# IViewMapperFactory is looked up as the result of view config
# in phase 3
- self.action(IViewMapperFactory, register, order=PHASE1_CONFIG)
+ intr = self.introspectable('view mappers',
+ IViewMapperFactory,
+ self.object_description(mapper),
+ 'default view mapper')
+ intr['mapper'] = mapper
+ self.action(IViewMapperFactory, register, order=PHASE1_CONFIG,
+ introspectables=(intr,))
@action_method
def add_static_view(self, name, path, **kw):
diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py
index 5e7137345..0261ae3db 100644
--- a/pyramid/interfaces.py
+++ b/pyramid/interfaces.py
@@ -859,9 +859,164 @@ class IRendererInfo(Interface):
'to the current application')
+class IIntrospector(Interface):
+ def get(category_name, discriminator, default=None):
+ """ Get the IIntrospectable related to the category_name and the
+ discriminator (or discriminator hash) ``discriminator``. If it does
+ not exist in the introspector, return the value of ``default`` """
+
+ def get_category(category_name, default=None, sort_key=None):
+ """ Get a sequence of dictionaries in the form
+ ``[{'introspectable':IIntrospectable, 'related':[sequence of related
+ IIntrospectables]}, ...]`` where each introspectable is part of the
+ category associated with ``category_name`` .
+
+ If the category named ``category_name`` does not exist in the
+ introspector the value passed as ``default`` will be returned.
+
+ If ``sort_key`` is ``None``, the sequence will be returned in the
+ order the introspectables were added to the introspector. Otherwise,
+ sort_key should be a function that accepts an IIntrospectable and
+ returns a value from it (ala the ``key`` function of Python's
+ ``sorted`` callable)."""
+
+ def categories():
+ """ Return a sorted sequence of category names known by
+ this introspector """
+
+ def categorized(sort_key=None):
+ """ Get a sequence of tuples in the form ``[(category_name,
+ [{'introspectable':IIntrospectable, 'related':[sequence of related
+ IIntrospectables]}, ...])]`` representing all known
+ introspectables. If ``sort_key`` is ``None``, each introspectables
+ sequence will be returned in the order the introspectables were added
+ to the introspector. Otherwise, sort_key should be a function that
+ accepts an IIntrospectable and returns a value from it (ala the
+ ``key`` function of Python's ``sorted`` callable)."""
+
+ def remove(category_name, discriminator):
+ """ Remove the IIntrospectable related to ``category_name`` and
+ ``discriminator`` from the introspector, and fix up any relations
+ that the introspectable participates in. This method will not raise
+ an error if an introspectable related to the category name and
+ discriminator does not exist."""
+
+ def related(intr):
+ """ Return a sequence of IIntrospectables related to the
+ IIntrospectable ``intr``. Return the empty sequence if no relations
+ for exist."""
+
+ def add(intr):
+ """ Add the IIntrospectable ``intr`` (use instead of
+ :meth:`pyramid.interfaces.IIntrospector.add` when you have a custom
+ IIntrospectable). Replaces any existing introspectable registered
+ using the same category/discriminator.
+
+ This method is not typically called directly, instead it's called
+ indirectly by :meth:`pyramid.interfaces.IIntrospector.register`"""
+
+ def relate(*pairs):
+ """ Given any number of of ``(category_name, discriminator)`` pairs
+ passed as positional arguments, relate the associated introspectables
+ to each other. The introspectable related to each pair must have
+ already been added via ``.add`` or ``.add_intr``; a :exc:`KeyError`
+ will result if this is not true. An error will not be raised if any
+ pair has already been associated with another.
+
+ This method is not typically called directly, instead it's called
+ indirectly by :meth:`pyramid.interfaces.IIntrospector.register`
+ """
+
+ def unrelate(*pairs):
+ """ Given any number of of ``(category_name, discriminator)`` pairs
+ passed as positional arguments, unrelate the associated introspectables
+ from each other. The introspectable related to each pair must have
+ already been added via ``.add`` or ``.add_intr``; a :exc:`KeyError`
+ will result if this is not true. An error will not be raised if any
+ pair is not already related to another.
+
+ This method is not typically called directly, instead it's called
+ indirectly by :meth:`pyramid.interfaces.IIntrospector.register`
+ """
+
+
+class IIntrospectable(Interface):
+ """ An introspectable object used for configuration introspection. In
+ addition to the methods below, objects which implement this interface
+ must also implement all the methods of Python's
+ ``collections.MutableMapping`` (the "dictionary interface"), and must be
+ hashable."""
+
+ title = Attribute('Text title describing this introspectable')
+ type_name = Attribute('Text type name describing this introspectable')
+ order = Attribute('integer order in which registered with introspector '
+ '(managed by introspector, usually)')
+ category_name = Attribute('introspection category name')
+ discriminator = Attribute('introspectable discriminator (within category) '
+ '(must be hashable)')
+ discriminator_hash = Attribute('an integer hash of the discriminator')
+ action_info = Attribute('An IActionInfo object representing the caller '
+ 'that invoked the creation of this introspectable '
+ '(usually a sentinel until updated during '
+ 'self.register)')
+
+ def relate(category_name, discriminator):
+ """ Indicate an intent to relate this IIntrospectable with another
+ IIntrospectable (the one associated with the ``category_name`` and
+ ``discriminator``) during action execution.
+ """
+
+ def unrelate(category_name, discriminator):
+ """ Indicate an intent to break the relationship between this
+ IIntrospectable with another IIntrospectable (the one associated with
+ the ``category_name`` and ``discriminator``) during action execution.
+ """
+
+ def register(introspector, action_info):
+ """ Register this IIntrospectable with an introspector. This method
+ is invoked during action execution. Adds the introspectable and its
+ relations to the introspector. ``introspector`` should be an object
+ implementing IIntrospector. ``action_info`` should be a object
+ implementing the interface :class:`pyramid.interfaces.IActionInfo`
+ representing the call that registered this introspectable.
+ Pseudocode for an implementation of this method:
+
+ .. code-block:: python
+
+ def register(self, introspector, action_info):
+ self.action_info = action_info
+ introspector.add(self)
+ for methodname, category_name, discriminator in self._relations:
+ method = getattr(introspector, methodname)
+ method((i.category_name, i.discriminator),
+ (category_name, discriminator))
+ """
+
+ def __hash__():
+
+ """ Introspectables must be hashable. The typical implementation of
+ an introsepectable's __hash__ is::
+
+ return hash((self.category_name,) + (self.discriminator,))
+ """
+
+class IActionInfo(Interface):
+ """ Class which provides code introspection capability associated with an
+ action. The ParserInfo class used by ZCML implements the same interface."""
+ file = Attribute(
+ 'Filename of action-invoking code as a string')
+ line = Attribute(
+ 'Starting line number in file (as an integer) of action-invoking code.'
+ 'This will be ``None`` if the value could not be determined.')
+
+ def __str__():
+ """ Return a representation of the action information (including
+ source code from file, if possible) """
+
# configuration phases: a lower phase number means the actions associated
# with this phase will be executed earlier than those with later phase
# numbers. The default phase number is 0, FTR.
PHASE1_CONFIG = -20
PHASE2_CONFIG = -10
+
diff --git a/pyramid/registry.py b/pyramid/registry.py
index ac706595e..7e373b58a 100644
--- a/pyramid/registry.py
+++ b/pyramid/registry.py
@@ -1,7 +1,16 @@
+import operator
+
+from zope.interface import implementer
+
from zope.interface.registry import Components
from pyramid.compat import text_
-from pyramid.interfaces import ISettings
+
+from pyramid.interfaces import (
+ ISettings,
+ IIntrospector,
+ IIntrospectable,
+ )
empty = text_('')
@@ -26,6 +35,7 @@ class Registry(Components, dict):
# for optimization purposes, if no listeners are listening, don't try
# to notify them
has_listeners = False
+
_settings = None
def __nonzero__(self):
@@ -74,4 +84,163 @@ class Registry(Components, dict):
settings = property(_get_settings, _set_settings)
+@implementer(IIntrospector)
+class Introspector(object):
+ def __init__(self):
+ self._refs = {}
+ self._categories = {}
+ self._counter = 0
+
+ def add(self, intr):
+ category = self._categories.setdefault(intr.category_name, {})
+ category[intr.discriminator] = intr
+ category[intr.discriminator_hash] = intr
+ intr.order = self._counter
+ self._counter += 1
+
+ def get(self, category_name, discriminator, default=None):
+ category = self._categories.setdefault(category_name, {})
+ intr = category.get(discriminator, default)
+ return intr
+
+ def get_category(self, category_name, default=None, sort_key=None):
+ if sort_key is None:
+ sort_key = operator.attrgetter('order')
+ category = self._categories.get(category_name)
+ if category is None:
+ return default
+ values = category.values()
+ values = sorted(set(values), key=sort_key)
+ return [
+ {'introspectable':intr,
+ 'related':self.related(intr)}
+ for intr in values
+ ]
+
+ def categorized(self, sort_key=None):
+ L = []
+ for category_name in self.categories():
+ L.append((category_name, self.get_category(category_name,
+ sort_key=sort_key)))
+ return L
+
+ def categories(self):
+ return sorted(self._categories.keys())
+
+ def remove(self, category_name, discriminator):
+ intr = self.get(category_name, discriminator)
+ if intr is None:
+ return
+ L = self._refs.pop(intr, [])
+ for d in L:
+ L2 = self._refs[d]
+ L2.remove(intr)
+ category = self._categories[intr.category_name]
+ del category[intr.discriminator]
+ del category[intr.discriminator_hash]
+
+ def _get_intrs_by_pairs(self, pairs):
+ introspectables = []
+ for pair in pairs:
+ category_name, discriminator = pair
+ intr = self._categories.get(category_name, {}).get(discriminator)
+ if intr is None:
+ raise KeyError((category_name, discriminator))
+ introspectables.append(intr)
+ return introspectables
+
+ def relate(self, *pairs):
+ introspectables = self._get_intrs_by_pairs(pairs)
+ relatable = ((x,y) for x in introspectables for y in introspectables)
+ for x, y in relatable:
+ L = self._refs.setdefault(x, [])
+ if x is not y and y not in L:
+ L.append(y)
+
+ def unrelate(self, *pairs):
+ introspectables = self._get_intrs_by_pairs(pairs)
+ relatable = ((x,y) for x in introspectables for y in introspectables)
+ for x, y in relatable:
+ L = self._refs.get(x, [])
+ if y in L:
+ L.remove(y)
+
+ def related(self, intr):
+ category_name, discriminator = intr.category_name, intr.discriminator
+ intr = self._categories.get(category_name, {}).get(discriminator)
+ if intr is None:
+ raise KeyError((category_name, discriminator))
+ return self._refs.get(intr, [])
+
+@implementer(IIntrospector)
+class _NoopIntrospector(object):
+ def add(self, intr):
+ pass
+ def get(self, category_name, discriminator, default=None):
+ return default
+ def get_category(self, category_name, default=None, sort_key=None):
+ return default
+ def categorized(self, sort_key=None):
+ return []
+ def categories(self):
+ return []
+ def remove(self, category_name, discriminator):
+ return
+ def relate(self, *pairs):
+ return
+ unrelate = relate
+ def related(self, intr):
+ return []
+
+noop_introspector = _NoopIntrospector()
+
+@implementer(IIntrospectable)
+class Introspectable(dict):
+
+ order = 0 # mutated by introspector.add
+ action_info = None # mutated by self.register
+
+ def __init__(self, category_name, discriminator, title, type_name):
+ self.category_name = category_name
+ self.discriminator = discriminator
+ self.title = title
+ self.type_name = type_name
+ self._relations = []
+
+ def relate(self, category_name, discriminator):
+ self._relations.append((True, category_name, discriminator))
+
+ def unrelate(self, category_name, discriminator):
+ self._relations.append((False, category_name, discriminator))
+
+ @property
+ def discriminator_hash(self):
+ return hash(self.discriminator)
+
+ def __hash__(self):
+ return hash((self.category_name,) + (self.discriminator,))
+
+ def __repr__(self):
+ return '<%s category %r, discriminator %r>' % (self.__class__.__name__,
+ self.category_name,
+ self.discriminator)
+
+ def __nonzero__(self):
+ return True
+
+ __bool__ = __nonzero__ # py3
+
+ def register(self, introspector, action_info):
+ self.action_info = action_info
+ introspector.add(self)
+ for relate, category_name, discriminator in self._relations:
+ if relate:
+ method = introspector.relate
+ else:
+ method = introspector.unrelate
+ method(
+ (self.category_name, self.discriminator),
+ (category_name, discriminator)
+ )
+
global_registry = Registry('global')
diff --git a/pyramid/router.py b/pyramid/router.py
index 608a66756..0c115a1ac 100644
--- a/pyramid/router.py
+++ b/pyramid/router.py
@@ -189,7 +189,7 @@ class Router(object):
if request.response_callbacks:
request._process_response_callbacks(response)
-
+
return response(request.environ, start_response)
finally:
diff --git a/pyramid/scripts/pcreate.py b/pyramid/scripts/pcreate.py
index f559e4f17..dacebd6ea 100644
--- a/pyramid/scripts/pcreate.py
+++ b/pyramid/scripts/pcreate.py
@@ -24,6 +24,12 @@ class PCreateCommand(object):
action='append',
help=("Add a scaffold to the create process "
"(multiple -s args accepted)"))
+ parser.add_option('-t', '--template',
+ dest='scaffold_name',
+ action='append',
+ help=('A backwards compatibility alias for '
+ '-s/--scaffold. Add a scaffold to the '
+ 'create process (multiple -t args accepted)'))
parser.add_option('-l', '--list',
dest='list',
action='store_true',
diff --git a/pyramid/tests/test_config/test_init.py b/pyramid/tests/test_config/test_init.py
index ca1508295..17dacdc5b 100644
--- a/pyramid/tests/test_config/test_init.py
+++ b/pyramid/tests/test_config/test_init.py
@@ -228,6 +228,12 @@ class ConfiguratorTests(unittest.TestCase):
request_iface=IRequest)
self.assertTrue(view.__wraps__ is exceptionresponse_view)
+ def test_ctor_with_introspector(self):
+ introspector = DummyIntrospector()
+ config = self._makeOne(introspector=introspector)
+ self.assertEqual(config.introspector, introspector)
+ self.assertEqual(config.registry.introspector, introspector)
+
def test_with_package_module(self):
from pyramid.tests.test_config import test_init
import pyramid.tests
@@ -637,6 +643,21 @@ pyramid.tests.test_config.dummy_include2""",
[('pyramid.tests.test_config.dummy_tween_factory',
dummy_tween_factory)])
+ def test_introspector_decorator(self):
+ inst = self._makeOne()
+ default = inst.introspector
+ self.failUnless(hasattr(default, 'add'))
+ self.assertEqual(inst.introspector, inst.registry.introspector)
+ introspector = DummyIntrospector()
+ inst.introspector = introspector
+ new = inst.introspector
+ self.failUnless(new is introspector)
+ self.assertEqual(inst.introspector, inst.registry.introspector)
+ del inst.introspector
+ default = inst.introspector
+ self.failIf(default is new)
+ self.failUnless(hasattr(default, 'add'))
+
def test_make_wsgi_app(self):
import pyramid.config
from pyramid.router import Router
@@ -662,10 +683,10 @@ pyramid.tests.test_config.dummy_include2""",
after = config.action_state
actions = after.actions
self.assertEqual(len(actions), 1)
- self.assertEqual(
- after.actions[0][:3],
- ('discrim', None, test_config),
- )
+ action = after.actions[0]
+ self.assertEqual(action['discriminator'], 'discrim')
+ self.assertEqual(action['callable'], None)
+ self.assertEqual(action['args'], test_config)
def test_include_with_python_callable(self):
from pyramid.tests import test_config
@@ -674,10 +695,10 @@ pyramid.tests.test_config.dummy_include2""",
after = config.action_state
actions = after.actions
self.assertEqual(len(actions), 1)
- self.assertEqual(
- actions[0][:3],
- ('discrim', None, test_config),
- )
+ action = actions[0]
+ self.assertEqual(action['discriminator'], 'discrim')
+ self.assertEqual(action['callable'], None)
+ self.assertEqual(action['args'], test_config)
def test_include_with_module_defaults_to_includeme(self):
from pyramid.tests import test_config
@@ -686,10 +707,10 @@ pyramid.tests.test_config.dummy_include2""",
after = config.action_state
actions = after.actions
self.assertEqual(len(actions), 1)
- self.assertEqual(
- actions[0][:3],
- ('discrim', None, test_config),
- )
+ action = actions[0]
+ self.assertEqual(action['discriminator'], 'discrim')
+ self.assertEqual(action['callable'], None)
+ self.assertEqual(action['args'], test_config)
def test_include_with_route_prefix(self):
root_config = self._makeOne(autocommit=True)
@@ -740,6 +761,15 @@ pyramid.tests.test_config.dummy_include2""",
config = self._makeOne(autocommit=True)
self.assertEqual(config.action('discrim', kw={'a':1}), None)
+ def test_action_autocommit_with_introspectables(self):
+ from pyramid.config.util import ActionInfo
+ config = self._makeOne(autocommit=True)
+ intr = DummyIntrospectable()
+ config.action('discrim', introspectables=(intr,))
+ self.assertEqual(len(intr.registered), 1)
+ self.assertEqual(intr.registered[0][0], config.introspector)
+ self.assertEqual(intr.registered[0][1].__class__, ActionInfo)
+
def test_action_branching_nonautocommit_with_config_info(self):
config = self._makeOne(autocommit=False)
config.info = 'abc'
@@ -749,9 +779,15 @@ pyramid.tests.test_config.dummy_include2""",
config.action('discrim', kw={'a':1})
self.assertEqual(
state.actions,
- [(('discrim', None, (), {'a': 1}, 0),
- {'info': 'abc', 'includepath':()})]
- )
+ [((),
+ {'args': (),
+ 'callable': None,
+ 'discriminator': 'discrim',
+ 'includepath': (),
+ 'info': 'abc',
+ 'introspectables': (),
+ 'kw': {'a': 1},
+ 'order': 0})])
def test_action_branching_nonautocommit_without_config_info(self):
config = self._makeOne(autocommit=False)
@@ -763,9 +799,27 @@ pyramid.tests.test_config.dummy_include2""",
config.action('discrim', kw={'a':1})
self.assertEqual(
state.actions,
- [(('discrim', None, (), {'a': 1}, 0),
- {'info': 'z', 'includepath':()})]
- )
+ [((),
+ {'args': (),
+ 'callable': None,
+ 'discriminator': 'discrim',
+ 'includepath': (),
+ 'info': 'z',
+ 'introspectables': (),
+ 'kw': {'a': 1},
+ 'order': 0})])
+
+ def test_action_branching_nonautocommit_with_introspectables(self):
+ config = self._makeOne(autocommit=False)
+ config.info = ''
+ config._ainfo = []
+ state = DummyActionState()
+ config.action_state = state
+ state.autocommit = False
+ intr = DummyIntrospectable()
+ config.action('discrim', introspectables=(intr,))
+ self.assertEqual(
+ state.actions[0][1]['introspectables'], (intr,))
def test_scan_integration(self):
from zope.interface import alsoProvides
@@ -922,7 +976,7 @@ pyramid.tests.test_config.dummy_include2""",
conflicts = e._conflicts.values()
for conflict in conflicts:
for confinst in conflict:
- yield confinst[3]
+ yield confinst.src
which = list(scanconflicts(why))
self.assertEqual(len(which), 4)
self.assertTrue("@view_config(renderer='string')" in which)
@@ -1313,10 +1367,10 @@ class TestConfigurator_add_directive(unittest.TestCase):
self.assertTrue(hasattr(config, 'dummy_extend'))
config.dummy_extend('discrim')
after = config.action_state
- self.assertEqual(
- after.actions[-1][:3],
- ('discrim', None, test_config),
- )
+ action = after.actions[-1]
+ self.assertEqual(action['discriminator'], 'discrim')
+ self.assertEqual(action['callable'], None)
+ self.assertEqual(action['args'], test_config)
def test_extend_with_python_callable(self):
from pyramid.tests import test_config
@@ -1326,10 +1380,10 @@ class TestConfigurator_add_directive(unittest.TestCase):
self.assertTrue(hasattr(config, 'dummy_extend'))
config.dummy_extend('discrim')
after = config.action_state
- self.assertEqual(
- after.actions[-1][:3],
- ('discrim', None, test_config),
- )
+ action = after.actions[-1]
+ self.assertEqual(action['discriminator'], 'discrim')
+ self.assertEqual(action['callable'], None)
+ self.assertEqual(action['args'], test_config)
def test_extend_same_name_doesnt_conflict(self):
config = self.config
@@ -1340,10 +1394,10 @@ class TestConfigurator_add_directive(unittest.TestCase):
self.assertTrue(hasattr(config, 'dummy_extend'))
config.dummy_extend('discrim')
after = config.action_state
- self.assertEqual(
- after.actions[-1][:3],
- ('discrim', None, config.registry),
- )
+ action = after.actions[-1]
+ self.assertEqual(action['discriminator'], 'discrim')
+ self.assertEqual(action['callable'], None)
+ self.assertEqual(action['args'], config.registry)
def test_extend_action_method_successful(self):
config = self.config
@@ -1361,10 +1415,10 @@ class TestConfigurator_add_directive(unittest.TestCase):
after = config2.action_state
actions = after.actions
self.assertEqual(len(actions), 1)
- self.assertEqual(
- after.actions[0][:3],
- ('discrim', None, config2.package),
- )
+ action = actions[0]
+ self.assertEqual(action['discriminator'], 'discrim')
+ self.assertEqual(action['callable'], None)
+ self.assertEqual(action['args'], config2.package)
class TestActionState(unittest.TestCase):
def _makeOne(self):
@@ -1380,42 +1434,120 @@ class TestActionState(unittest.TestCase):
c = self._makeOne()
c.actions = []
c.action(1, f, (1,), {'x':1})
- self.assertEqual(c.actions, [(1, f, (1,), {'x': 1})])
+ self.assertEqual(
+ c.actions,
+ [{'args': (1,),
+ 'callable': f,
+ 'discriminator': 1,
+ 'includepath': (),
+ 'info': None,
+ 'introspectables': (),
+ 'kw': {'x': 1},
+ 'order': 0}])
c.action(None)
- self.assertEqual(c.actions, [(1, f, (1,), {'x': 1}), (None, None)])
+ self.assertEqual(
+ c.actions,
+ [{'args': (1,),
+ 'callable': f,
+ 'discriminator': 1,
+ 'includepath': (),
+ 'info': None,
+ 'introspectables': (),
+ 'kw': {'x': 1},
+ 'order': 0},
+
+ {'args': (),
+ 'callable': None,
+ 'discriminator': None,
+ 'includepath': (),
+ 'info': None,
+ 'introspectables': (),
+ 'kw': {},
+ 'order': 0},])
def test_action_with_includepath(self):
c = self._makeOne()
c.actions = []
c.action(None, includepath=('abc',))
- self.assertEqual(c.actions, [(None, None, (), {}, ('abc',))])
+ self.assertEqual(
+ c.actions,
+ [{'args': (),
+ 'callable': None,
+ 'discriminator': None,
+ 'includepath': ('abc',),
+ 'info': None,
+ 'introspectables': (),
+ 'kw': {},
+ 'order': 0}])
def test_action_with_info(self):
c = self._makeOne()
c.action(None, info='abc')
- self.assertEqual(c.actions, [(None, None, (), {}, (), 'abc')])
+ self.assertEqual(
+ c.actions,
+ [{'args': (),
+ 'callable': None,
+ 'discriminator': None,
+ 'includepath': (),
+ 'info': 'abc',
+ 'introspectables': (),
+ 'kw': {},
+ 'order': 0}])
def test_action_with_includepath_and_info(self):
c = self._makeOne()
c.action(None, includepath=('spec',), info='bleh')
- self.assertEqual(c.actions,
- [(None, None, (), {}, ('spec',), 'bleh')])
+ self.assertEqual(
+ c.actions,
+ [{'args': (),
+ 'callable': None,
+ 'discriminator': None,
+ 'includepath': ('spec',),
+ 'info': 'bleh',
+ 'introspectables': (),
+ 'kw': {},
+ 'order': 0}])
def test_action_with_order(self):
c = self._makeOne()
c.actions = []
c.action(None, order=99999)
- self.assertEqual(c.actions, [(None, None, (), {}, (), '', 99999)])
+ self.assertEqual(
+ c.actions,
+ [{'args': (),
+ 'callable': None,
+ 'discriminator': None,
+ 'includepath': (),
+ 'info': None,
+ 'introspectables': (),
+ 'kw': {},
+ 'order': 99999}])
+
+ def test_action_with_introspectables(self):
+ c = self._makeOne()
+ c.actions = []
+ intr = DummyIntrospectable()
+ c.action(None, introspectables=(intr,))
+ self.assertEqual(
+ c.actions,
+ [{'args': (),
+ 'callable': None,
+ 'discriminator': None,
+ 'includepath': (),
+ 'info': None,
+ 'introspectables': (intr,),
+ 'kw': {},
+ 'order': 0}])
def test_processSpec(self):
c = self._makeOne()
self.assertTrue(c.processSpec('spec'))
self.assertFalse(c.processSpec('spec'))
- def test_execute_actions_simple(self):
+ def test_execute_actions_tuples(self):
output = []
def f(*a, **k):
- output.append(('f', a, k))
+ output.append((a, k))
c = self._makeOne()
c.actions = [
(1, f, (1,)),
@@ -1424,7 +1556,57 @@ class TestActionState(unittest.TestCase):
(None, None),
]
c.execute_actions()
- self.assertEqual(output, [('f', (1,), {}), ('f', (2,), {})])
+ self.assertEqual(output, [((1,), {}), ((2,), {})])
+
+ def test_execute_actions_dicts(self):
+ output = []
+ def f(*a, **k):
+ output.append((a, k))
+ c = self._makeOne()
+ c.actions = [
+ {'discriminator':1, 'callable':f, 'args':(1,), 'kw':{},
+ 'order':0, 'includepath':(), 'info':None,
+ 'introspectables':()},
+ {'discriminator':1, 'callable':f, 'args':(11,), 'kw':{},
+ 'includepath':('x',), 'order': 0, 'info':None,
+ 'introspectables':()},
+ {'discriminator':2, 'callable':f, 'args':(2,), 'kw':{},
+ 'order':0, 'includepath':(), 'info':None,
+ 'introspectables':()},
+ {'discriminator':None, 'callable':None, 'args':(), 'kw':{},
+ 'order':0, 'includepath':(), 'info':None,
+ 'introspectables':()},
+ ]
+ c.execute_actions()
+ self.assertEqual(output, [((1,), {}), ((2,), {})])
+
+ def test_execute_actions_with_introspectables(self):
+ output = []
+ def f(*a, **k):
+ output.append((a, k))
+ c = self._makeOne()
+ intr = DummyIntrospectable()
+ c.actions = [
+ {'discriminator':1, 'callable':f, 'args':(1,), 'kw':{},
+ 'order':0, 'includepath':(), 'info':None,
+ 'introspectables':(intr,)},
+ ]
+ introspector = DummyIntrospector()
+ c.execute_actions(introspector=introspector)
+ self.assertEqual(output, [((1,), {})])
+ self.assertEqual(intr.registered, [(introspector, None)])
+
+ def test_execute_actions_with_introspectable_no_callable(self):
+ c = self._makeOne()
+ intr = DummyIntrospectable()
+ c.actions = [
+ {'discriminator':1, 'callable':None, 'args':(1,), 'kw':{},
+ 'order':0, 'includepath':(), 'info':None,
+ 'introspectables':(intr,)},
+ ]
+ introspector = DummyIntrospector()
+ c.execute_actions(introspector=introspector)
+ self.assertEqual(intr.registered, [(introspector, None)])
def test_execute_actions_error(self):
output = []
@@ -1447,7 +1629,7 @@ class Test_resolveConflicts(unittest.TestCase):
from pyramid.config import resolveConflicts
return resolveConflicts(actions)
- def test_it_success(self):
+ def test_it_success_tuples(self):
from pyramid.tests.test_config import dummyfactory as f
result = self._callFUT([
(None, f),
@@ -1458,12 +1640,115 @@ class Test_resolveConflicts(unittest.TestCase):
(3, f, (3,), {}, ('y',)),
(None, f, (5,), {}, ('y',)),
])
- self.assertEqual(result,
- [(None, f),
- (1, f, (1,), {}, (), 'first'),
- (3, f, (3,), {}, ('y',)),
- (None, f, (5,), {}, ('y',)),
- (4, f, (4,), {}, ('y',), 'should be last')])
+ self.assertEqual(
+ result,
+ [{'info': None,
+ 'args': (),
+ 'callable': f,
+ 'introspectables': (),
+ 'kw': {},
+ 'discriminator': None,
+ 'includepath': (),
+ 'order': 0},
+
+ {'info': 'first',
+ 'args': (1,),
+ 'callable': f,
+ 'introspectables': (),
+ 'kw': {},
+ 'discriminator': 1,
+ 'includepath': (),
+ 'order': 0},
+
+ {'info': None,
+ 'args': (3,),
+ 'callable': f,
+ 'introspectables': (),
+ 'kw': {},
+ 'discriminator': 3,
+ 'includepath': ('y',),
+ 'order': 0},
+
+ {'info': None,
+ 'args': (5,),
+ 'callable': f,
+ 'introspectables': (),
+ 'kw': {},
+ 'discriminator': None,
+ 'includepath': ('y',),
+ 'order': 0},
+
+ {'info': 'should be last',
+ 'args': (4,),
+ 'callable': f,
+ 'introspectables': (),
+ 'kw': {},
+ 'discriminator': 4,
+ 'includepath': ('y',),
+ 'order': 99999}
+ ]
+ )
+
+ def test_it_success_dicts(self):
+ from pyramid.tests.test_config import dummyfactory as f
+ from pyramid.config import expand_action
+ result = self._callFUT([
+ expand_action(None, f),
+ expand_action(1, f, (1,), {}, (), 'first'),
+ expand_action(1, f, (2,), {}, ('x',), 'second'),
+ expand_action(1, f, (3,), {}, ('y',), 'third'),
+ expand_action(4, f, (4,), {}, ('y',), 'should be last', 99999),
+ expand_action(3, f, (3,), {}, ('y',)),
+ expand_action(None, f, (5,), {}, ('y',)),
+ ])
+ self.assertEqual(
+ result,
+ [{'info': None,
+ 'args': (),
+ 'callable': f,
+ 'introspectables': (),
+ 'kw': {},
+ 'discriminator': None,
+ 'includepath': (),
+ 'order': 0},
+
+ {'info': 'first',
+ 'args': (1,),
+ 'callable': f,
+ 'introspectables': (),
+ 'kw': {},
+ 'discriminator': 1,
+ 'includepath': (),
+ 'order': 0},
+
+ {'info': None,
+ 'args': (3,),
+ 'callable': f,
+ 'introspectables': (),
+ 'kw': {},
+ 'discriminator': 3,
+ 'includepath': ('y',),
+ 'order': 0},
+
+ {'info': None,
+ 'args': (5,),
+ 'callable': f,
+ 'introspectables': (),
+ 'kw': {},
+ 'discriminator': None,
+ 'includepath': ('y',),
+ 'order': 0},
+
+ {'info': 'should be last',
+ 'args': (4,),
+ 'callable': f,
+ 'introspectables': (),
+ 'kw': {},
+ 'discriminator': 4,
+ 'includepath': ('y',),
+ 'order': 99999}
+ ]
+ )
def test_it_conflict(self):
from pyramid.tests.test_config import dummyfactory as f
@@ -1479,6 +1764,81 @@ class Test_resolveConflicts(unittest.TestCase):
]
)
+ def test_it_with_actions_grouped_by_order(self):
+ from pyramid.tests.test_config import dummyfactory as f
+ from pyramid.config import expand_action
+ result = self._callFUT([
+ expand_action(None, f),
+ expand_action(1, f, (1,), {}, (), 'third', 10),
+ expand_action(1, f, (2,), {}, ('x',), 'fourth', 10),
+ expand_action(1, f, (3,), {}, ('y',), 'fifth', 10),
+ expand_action(2, f, (1,), {}, (), 'sixth', 10),
+ expand_action(3, f, (1,), {}, (), 'seventh', 10),
+ expand_action(5, f, (4,), {}, ('y',), 'eighth', 99999),
+ expand_action(4, f, (3,), {}, (), 'first', 5),
+ expand_action(4, f, (5,), {}, ('y',), 'second', 5),
+ ])
+ self.assertEqual(len(result), 6)
+ # resolved actions should be grouped by (order, i)
+ self.assertEqual(
+ result,
+ [{'info': None,
+ 'args': (),
+ 'callable': f,
+ 'introspectables': (),
+ 'kw': {},
+ 'discriminator': None,
+ 'includepath': (),
+ 'order': 0},
+
+ {'info': 'first',
+ 'args': (3,),
+ 'callable': f,
+ 'introspectables': (),
+ 'kw': {},
+ 'discriminator': 4,
+ 'includepath': (),
+ 'order': 5},
+
+ {'info': 'third',
+ 'args': (1,),
+ 'callable': f,
+ 'introspectables': (),
+ 'kw': {},
+ 'discriminator': 1,
+ 'includepath': (),
+ 'order': 10},
+
+ {'info': 'sixth',
+ 'args': (1,),
+ 'callable': f,
+ 'introspectables': (),
+ 'kw': {},
+ 'discriminator': 2,
+ 'includepath': (),
+ 'order': 10},
+
+ {'info': 'seventh',
+ 'args': (1,),
+ 'callable': f,
+ 'introspectables': (),
+ 'kw': {},
+ 'discriminator': 3,
+ 'includepath': (),
+ 'order': 10},
+
+ {'info': 'eighth',
+ 'args': (4,),
+ 'callable': f,
+ 'introspectables': (),
+ 'kw': {},
+ 'discriminator': 5,
+ 'includepath': ('y',),
+ 'order': 99999}
+ ]
+ )
+
+
class TestGlobalRegistriesIntegration(unittest.TestCase):
def setUp(self):
from pyramid.config import global_registries
@@ -1565,7 +1925,7 @@ def _conflictFunctions(e):
conflicts = e._conflicts.values()
for conflict in conflicts:
for confinst in conflict:
- yield confinst[2]
+ yield confinst.function
class DummyActionState(object):
autocommit = False
@@ -1584,3 +1944,15 @@ class DummyZCMLContext(object):
includepath = ()
info = ''
+class DummyIntrospector(object):
+ def __init__(self):
+ self.intrs = []
+ def add(self, intr):
+ self.intrs.append(intr)
+
+class DummyIntrospectable(object):
+ def __init__(self):
+ self.registered = []
+ def register(self, introspector, action_info):
+ self.registered.append((introspector, action_info))
+
diff --git a/pyramid/tests/test_config/test_routes.py b/pyramid/tests/test_config/test_routes.py
index 1646561cd..140a4aa73 100644
--- a/pyramid/tests/test_config/test_routes.py
+++ b/pyramid/tests/test_config/test_routes.py
@@ -52,7 +52,8 @@ class RoutesConfiguratorMixinTests(unittest.TestCase):
def test_add_route_discriminator(self):
config = self._makeOne()
config.add_route('name', 'path')
- self.assertEqual(config.action_state.actions[-1][0], ('route', 'name'))
+ self.assertEqual(config.action_state.actions[-1]['discriminator'],
+ ('route', 'name'))
def test_add_route_with_factory(self):
config = self._makeOne(autocommit=True)
diff --git a/pyramid/tests/test_config/test_util.py b/pyramid/tests/test_config/test_util.py
index bc7cf0a82..1180e7e29 100644
--- a/pyramid/tests/test_config/test_util.py
+++ b/pyramid/tests/test_config/test_util.py
@@ -312,6 +312,36 @@ class Test__make_predicates(unittest.TestCase):
hash2, _, __= self._callFUT(request_method='GET')
self.assertEqual(hash1, hash2)
+class TestActionInfo(unittest.TestCase):
+ def _getTargetClass(self):
+ from pyramid.config.util import ActionInfo
+ return ActionInfo
+
+ def _makeOne(self, filename, lineno, function, linerepr):
+ return self._getTargetClass()(filename, lineno, function, linerepr)
+
+ def test_class_conforms(self):
+ from zope.interface.verify import verifyClass
+ from pyramid.interfaces import IActionInfo
+ verifyClass(IActionInfo, self._getTargetClass())
+
+ def test_instance_conforms(self):
+ from zope.interface.verify import verifyObject
+ from pyramid.interfaces import IActionInfo
+ verifyObject(IActionInfo, self._makeOne('f', 0, 'f', 'f'))
+
+ def test_ctor(self):
+ inst = self._makeOne('filename', 10, 'function', 'src')
+ self.assertEqual(inst.file, 'filename')
+ self.assertEqual(inst.line, 10)
+ self.assertEqual(inst.function, 'function')
+ self.assertEqual(inst.src, 'src')
+
+ def test___str__(self):
+ inst = self._makeOne('filename', 0, 'function', ' linerepr ')
+ self.assertEqual(str(inst),
+ "Line 0 of file filename:\n linerepr ")
+
class DummyCustomPredicate(object):
def __init__(self):
self.__text__ = 'custom predicate'
diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py
index a9a4d5836..d80a6bb64 100644
--- a/pyramid/tests/test_config/test_views.py
+++ b/pyramid/tests/test_config/test_views.py
@@ -1362,6 +1362,20 @@ class TestViewsConfigurationMixin(unittest.TestCase):
request = self._makeRequest(config)
self.assertEqual(view(None, request), 'OK')
+ def test_add_view_with_mapper(self):
+ from pyramid.renderers import null_renderer
+ class Mapper(object):
+ def __init__(self, **kw):
+ self.__class__.kw = kw
+ def __call__(self, view):
+ return view
+ config = self._makeOne(autocommit=True)
+ def view(context, request): return 'OK'
+ config.add_view(view=view, mapper=Mapper, renderer=null_renderer)
+ view = self._getViewCallable(config)
+ self.assertEqual(view(None, None), 'OK')
+ self.assertEqual(Mapper.kw['mapper'], Mapper)
+
def test_derive_view_function(self):
from pyramid.renderers import null_renderer
def view(request):
diff --git a/pyramid/tests/test_events.py b/pyramid/tests/test_events.py
index 108a5d2d9..4b58a129c 100644
--- a/pyramid/tests/test_events.py
+++ b/pyramid/tests/test_events.py
@@ -122,11 +122,10 @@ class ContextFoundEventTests(unittest.TestCase):
class TestSubscriber(unittest.TestCase):
def setUp(self):
- registry = DummyRegistry()
- self.config = testing.setUp(registry=registry)
+ self.config = testing.setUp()
def tearDown(self):
- self.config.end()
+ testing.tearDown()
def _makeOne(self, *ifaces):
from pyramid.events import subscriber
diff --git a/pyramid/tests/test_registry.py b/pyramid/tests/test_registry.py
index c3104bd31..29803346a 100644
--- a/pyramid/tests/test_registry.py
+++ b/pyramid/tests/test_registry.py
@@ -42,11 +42,362 @@ class TestRegistry(unittest.TestCase):
registry.settings = 'foo'
self.assertEqual(registry._settings, 'foo')
+class TestIntrospector(unittest.TestCase):
+ def _getTargetClass(slf):
+ from pyramid.registry import Introspector
+ return Introspector
+
+ def _makeOne(self):
+ return self._getTargetClass()()
+
+ def test_conformance(self):
+ from zope.interface.verify import verifyClass
+ from zope.interface.verify import verifyObject
+ from pyramid.interfaces import IIntrospector
+ verifyClass(IIntrospector, self._getTargetClass())
+ verifyObject(IIntrospector, self._makeOne())
+
+ def test_add(self):
+ inst = self._makeOne()
+ intr = DummyIntrospectable()
+ inst.add(intr)
+ self.assertEqual(intr.order, 0)
+ category = {'discriminator':intr, 'discriminator_hash':intr}
+ self.assertEqual(inst._categories, {'category':category})
+
+ def test_get_success(self):
+ inst = self._makeOne()
+ intr = DummyIntrospectable()
+ inst.add(intr)
+ self.assertEqual(inst.get('category', 'discriminator'), intr)
+
+ def test_get_success_byhash(self):
+ inst = self._makeOne()
+ intr = DummyIntrospectable()
+ inst.add(intr)
+ self.assertEqual(inst.get('category', 'discriminator_hash'), intr)
+
+ def test_get_fail(self):
+ inst = self._makeOne()
+ intr = DummyIntrospectable()
+ inst.add(intr)
+ self.assertEqual(inst.get('category', 'wontexist', 'foo'), 'foo')
+
+ def test_get_category(self):
+ inst = self._makeOne()
+ intr = DummyIntrospectable()
+ intr2 = DummyIntrospectable()
+ intr2.discriminator = 'discriminator2'
+ intr2.discriminator_hash = 'discriminator2_hash'
+ inst.add(intr2)
+ inst.add(intr)
+ expected = [
+ {'introspectable':intr2, 'related':[]},
+ {'introspectable':intr, 'related':[]},
+ ]
+ self.assertEqual(inst.get_category('category'), expected)
+
+ def test_get_category_returns_default_on_miss(self):
+ inst = self._makeOne()
+ self.assertEqual(inst.get_category('category', '123'), '123')
+
+ def test_get_category_with_sortkey(self):
+ import operator
+ inst = self._makeOne()
+ intr = DummyIntrospectable()
+ intr.foo = 2
+ intr2 = DummyIntrospectable()
+ intr2.discriminator = 'discriminator2'
+ intr2.discriminator_hash = 'discriminator2_hash'
+ intr2.foo = 1
+ inst.add(intr)
+ inst.add(intr2)
+ expected = [
+ {'introspectable':intr2, 'related':[]},
+ {'introspectable':intr, 'related':[]},
+ ]
+ self.assertEqual(
+ inst.get_category('category', sort_key=operator.attrgetter('foo')),
+ expected)
+
+ def test_categorized(self):
+ import operator
+ inst = self._makeOne()
+ intr = DummyIntrospectable()
+ intr.foo = 2
+ intr2 = DummyIntrospectable()
+ intr2.discriminator = 'discriminator2'
+ intr2.discriminator_hash = 'discriminator2_hash'
+ intr2.foo = 1
+ inst.add(intr)
+ inst.add(intr2)
+ expected = [('category', [
+ {'introspectable':intr2, 'related':[]},
+ {'introspectable':intr, 'related':[]},
+ ])]
+ self.assertEqual(
+ inst.categorized(sort_key=operator.attrgetter('foo')), expected)
+
+ def test_categories(self):
+ inst = self._makeOne()
+ inst._categories['a'] = 1
+ inst._categories['b'] = 2
+ self.assertEqual(list(inst.categories()), ['a', 'b'])
+
+ def test_remove(self):
+ inst = self._makeOne()
+ intr = DummyIntrospectable()
+ intr2 = DummyIntrospectable()
+ intr2.category_name = 'category2'
+ intr2.discriminator = 'discriminator2'
+ intr2.discriminator_hash = 'discriminator2_hash'
+ inst.add(intr)
+ inst.add(intr2)
+ inst.relate(('category', 'discriminator'),
+ ('category2', 'discriminator2'))
+ inst.remove('category', 'discriminator')
+ self.assertEqual(inst._categories,
+ {'category':
+ {},
+ 'category2':
+ {'discriminator2': intr2,
+ 'discriminator2_hash': intr2}
+ })
+ self.assertEqual(inst._refs.get(intr), None)
+ self.assertEqual(inst._refs[intr2], [])
+
+ def test_remove_fail(self):
+ inst = self._makeOne()
+ self.assertEqual(inst.remove('a', 'b'), None)
+
+ def test_relate(self):
+ inst = self._makeOne()
+ intr = DummyIntrospectable()
+ intr2 = DummyIntrospectable()
+ intr2.category_name = 'category2'
+ intr2.discriminator = 'discriminator2'
+ intr2.discriminator_hash = 'discriminator2_hash'
+ inst.add(intr)
+ inst.add(intr2)
+ inst.relate(('category', 'discriminator'),
+ ('category2', 'discriminator2'))
+ self.assertEqual(inst._categories,
+ {'category':
+ {'discriminator':intr,
+ 'discriminator_hash':intr},
+ 'category2':
+ {'discriminator2': intr2,
+ 'discriminator2_hash': intr2}
+ })
+ self.assertEqual(inst._refs[intr], [intr2])
+ self.assertEqual(inst._refs[intr2], [intr])
+
+ def test_relate_fail(self):
+ inst = self._makeOne()
+ intr = DummyIntrospectable()
+ inst.add(intr)
+ self.assertRaises(
+ KeyError,
+ inst.relate,
+ ('category', 'discriminator'),
+ ('category2', 'discriminator2')
+ )
+
+ def test_unrelate(self):
+ inst = self._makeOne()
+ intr = DummyIntrospectable()
+ intr2 = DummyIntrospectable()
+ intr2.category_name = 'category2'
+ intr2.discriminator = 'discriminator2'
+ intr2.discriminator_hash = 'discriminator2_hash'
+ inst.add(intr)
+ inst.add(intr2)
+ inst.relate(('category', 'discriminator'),
+ ('category2', 'discriminator2'))
+ inst.unrelate(('category', 'discriminator'),
+ ('category2', 'discriminator2'))
+ self.assertEqual(inst._categories,
+ {'category':
+ {'discriminator':intr,
+ 'discriminator_hash':intr},
+ 'category2':
+ {'discriminator2': intr2,
+ 'discriminator2_hash': intr2}
+ })
+ self.assertEqual(inst._refs[intr], [])
+ self.assertEqual(inst._refs[intr2], [])
+
+ def test_related(self):
+ inst = self._makeOne()
+ intr = DummyIntrospectable()
+ intr2 = DummyIntrospectable()
+ intr2.category_name = 'category2'
+ intr2.discriminator = 'discriminator2'
+ intr2.discriminator_hash = 'discriminator2_hash'
+ inst.add(intr)
+ inst.add(intr2)
+ inst.relate(('category', 'discriminator'),
+ ('category2', 'discriminator2'))
+ self.assertEqual(inst.related(intr), [intr2])
+
+ def test_related_fail(self):
+ inst = self._makeOne()
+ intr = DummyIntrospectable()
+ intr2 = DummyIntrospectable()
+ intr2.category_name = 'category2'
+ intr2.discriminator = 'discriminator2'
+ intr2.discriminator_hash = 'discriminator2_hash'
+ inst.add(intr)
+ inst.add(intr2)
+ inst.relate(('category', 'discriminator'),
+ ('category2', 'discriminator2'))
+ del inst._categories['category']
+ self.assertRaises(KeyError, inst.related, intr)
+
+class Test_noop_introspector(unittest.TestCase):
+ def _makeOne(self):
+ from pyramid.registry import noop_introspector
+ return noop_introspector
+
+ def test_conformance(self):
+ from zope.interface.verify import verifyObject
+ from pyramid.interfaces import IIntrospector
+ verifyObject(IIntrospector, self._makeOne())
+
+ def test_add(self):
+ inst = self._makeOne()
+ self.assertEqual(inst.add('a'), None)
+
+ def test_get(self):
+ inst = self._makeOne()
+ self.assertEqual(inst.get('category', 'd', default='123'), '123')
+
+ def test_get_category(self):
+ inst = self._makeOne()
+ self.assertEqual(inst.get_category('category', default='123'), '123')
+
+ def test_categorized(self):
+ inst = self._makeOne()
+ self.assertEqual(inst.categorized(), [])
+
+ def test_categories(self):
+ inst = self._makeOne()
+ self.assertEqual(inst.categories(), [])
+
+ def test_remove(self):
+ inst = self._makeOne()
+ self.assertEqual(inst.remove('cat', 'discrim'), None)
+
+ def test_relate(self):
+ inst = self._makeOne()
+ self.assertEqual(inst.relate(), None)
+
+ def test_unrelate(self):
+ inst = self._makeOne()
+ self.assertEqual(inst.unrelate(), None)
+
+ def test_related(self):
+ inst = self._makeOne()
+ self.assertEqual(inst.related('a'), [])
+
+class TestIntrospectable(unittest.TestCase):
+ def _getTargetClass(slf):
+ from pyramid.registry import Introspectable
+ return Introspectable
+
+ def _makeOne(self, *arg, **kw):
+ return self._getTargetClass()(*arg, **kw)
+
+ def _makeOnePopulated(self):
+ return self._makeOne('category', 'discrim', 'title', 'type')
+
+ def test_conformance(self):
+ from zope.interface.verify import verifyClass
+ from zope.interface.verify import verifyObject
+ from pyramid.interfaces import IIntrospectable
+ verifyClass(IIntrospectable, self._getTargetClass())
+ verifyObject(IIntrospectable, self._makeOnePopulated())
+
+ def test_relate(self):
+ inst = self._makeOnePopulated()
+ inst.relate('a', 'b')
+ self.assertEqual(inst._relations, [(True, 'a', 'b')])
+
+ def test_unrelate(self):
+ inst = self._makeOnePopulated()
+ inst.unrelate('a', 'b')
+ self.assertEqual(inst._relations, [(False, 'a', 'b')])
+
+ def test_discriminator_hash(self):
+ inst = self._makeOnePopulated()
+ self.assertEqual(inst.discriminator_hash, hash(inst.discriminator))
+
+ def test___hash__(self):
+ inst = self._makeOnePopulated()
+ self.assertEqual(hash(inst),
+ hash((inst.category_name,) + (inst.discriminator,)))
+
+ def test___repr__(self):
+ inst = self._makeOnePopulated()
+ self.assertEqual(
+ repr(inst),
+ "<Introspectable category 'category', discriminator 'discrim'>")
+
+ def test___nonzero__(self):
+ inst = self._makeOnePopulated()
+ self.assertEqual(inst.__nonzero__(), True)
+
+ def test___bool__(self):
+ inst = self._makeOnePopulated()
+ self.assertEqual(inst.__bool__(), True)
+
+ def test_register(self):
+ introspector = DummyIntrospector()
+ action_info = object()
+ inst = self._makeOnePopulated()
+ inst._relations.append((True, 'category1', 'discrim1'))
+ inst._relations.append((False, 'category2', 'discrim2'))
+ inst.register(introspector, action_info)
+ self.assertEqual(inst.action_info, action_info)
+ self.assertEqual(introspector.intrs, [inst])
+ self.assertEqual(introspector.relations,
+ [(('category', 'discrim'), ('category1', 'discrim1'))])
+ self.assertEqual(introspector.unrelations,
+ [(('category', 'discrim'), ('category2', 'discrim2'))])
+
+class DummyIntrospector(object):
+ def __init__(self):
+ self.intrs = []
+ self.relations = []
+ self.unrelations = []
+
+ def add(self, intr):
+ self.intrs.append(intr)
+
+ def relate(self, *pairs):
+ self.relations.append(pairs)
+
+ def unrelate(self, *pairs):
+ self.unrelations.append(pairs)
+
class DummyModule:
__path__ = "foo"
__name__ = "dummy"
__file__ = ''
+class DummyIntrospectable(object):
+ category_name = 'category'
+ discriminator = 'discriminator'
+ title = 'title'
+ type_name = 'type'
+ order = None
+ action_info = None
+ discriminator_hash = 'discriminator_hash'
+
+ def __hash__(self):
+ return hash((self.category_name,) + (self.discriminator,))
+
+
from zope.interface import Interface
from zope.interface import implementer
class IDummyEvent(Interface):
diff --git a/pyramid/tests/test_util.py b/pyramid/tests/test_util.py
index 57bcd08d7..61e372417 100644
--- a/pyramid/tests/test_util.py
+++ b/pyramid/tests/test_util.py
@@ -255,5 +255,78 @@ class Test_WeakOrderedSet(unittest.TestCase):
self.assertEqual(list(wos), [])
self.assertEqual(wos.last, None)
+class Test_object_description(unittest.TestCase):
+ def _callFUT(self, object):
+ from pyramid.util import object_description
+ return object_description(object)
+
+ def test_string(self):
+ self.assertEqual(self._callFUT('abc'), 'abc')
+
+ def test_int(self):
+ self.assertEqual(self._callFUT(1), '1')
+
+ def test_bool(self):
+ self.assertEqual(self._callFUT(True), 'True')
+
+ def test_None(self):
+ self.assertEqual(self._callFUT(None), 'None')
+
+ def test_float(self):
+ self.assertEqual(self._callFUT(1.2), '1.2')
+
+ def test_tuple(self):
+ self.assertEqual(self._callFUT(('a', 'b')), "('a', 'b')")
+
+ def test_set(self):
+ if PY3: # pragma: no cover
+ self.assertEqual(self._callFUT(set(['a'])), "{'a'}")
+ else: # pragma: no cover
+ self.assertEqual(self._callFUT(set(['a'])), "set(['a'])")
+
+ def test_list(self):
+ self.assertEqual(self._callFUT(['a']), "['a']")
+
+ def test_dict(self):
+ self.assertEqual(self._callFUT({'a':1}), "{'a': 1}")
+
+ def test_nomodule(self):
+ o = object()
+ self.assertEqual(self._callFUT(o), 'object %s' % str(o))
+
+ def test_module(self):
+ import pyramid
+ self.assertEqual(self._callFUT(pyramid), 'module pyramid')
+
+ def test_method(self):
+ self.assertEqual(
+ self._callFUT(self.test_method),
+ 'method test_method of class pyramid.tests.test_util.'
+ 'Test_object_description')
+
+ def test_class(self):
+ self.assertEqual(
+ self._callFUT(self.__class__),
+ 'class pyramid.tests.test_util.Test_object_description')
+
+ def test_function(self):
+ self.assertEqual(
+ self._callFUT(dummyfunc),
+ 'function pyramid.tests.test_util.dummyfunc')
+
+ def test_instance(self):
+ inst = Dummy()
+ self.assertEqual(
+ self._callFUT(inst),
+ "object %s" % str(inst))
+
+ def test_shortened_repr(self):
+ inst = ['1'] * 1000
+ self.assertEqual(
+ self._callFUT(inst),
+ str(inst)[:100] + ' ... ]')
+
+def dummyfunc(): pass
+
class Dummy(object):
pass
diff --git a/pyramid/util.py b/pyramid/util.py
index 3eb4b3fed..f22f847c4 100644
--- a/pyramid/util.py
+++ b/pyramid/util.py
@@ -1,8 +1,15 @@
+import inspect
import pkg_resources
import sys
import weakref
-from pyramid.compat import string_types
+from pyramid.compat import (
+ integer_types,
+ string_types,
+ text_,
+ PY3,
+ )
+
from pyramid.exceptions import ConfigurationError
from pyramid.path import package_of
@@ -228,3 +235,71 @@ def strings_differ(string1, string2):
return invalid_bits != 0
+def object_description(object):
+ """ Produce a human-consumable text description of ``object``,
+ usually involving a Python dotted name. For example:
+
+ .. code-block:: python
+
+ >>> object_description(None)
+ u'None'
+ >>> from xml.dom import minidom
+ >>> object_description(minidom)
+ u'module xml.dom.minidom'
+ >>> object_description(minidom.Attr)
+ u'class xml.dom.minidom.Attr'
+ >>> object_description(minidom.Attr.appendChild)
+ u'method appendChild of class xml.dom.minidom.Attr'
+ >>>
+
+ If this method cannot identify the type of the object, a generic
+ description ala ``object <object.__name__>`` will be returned.
+
+ If the object passed is already a string, it is simply returned. If it
+ is a boolean, an integer, a list, a tuple, a set, or ``None``, a
+ (possibly shortened) string representation is returned.
+ """
+ if isinstance(object, string_types):
+ return text_(object)
+ if isinstance(object, integer_types):
+ return text_(str(object))
+ if isinstance(object, (bool, float, type(None))):
+ return text_(str(object))
+ if isinstance(object, set):
+ if PY3: # pragma: no cover
+ return shortrepr(object, '}')
+ else:
+ return shortrepr(object, ')')
+ if isinstance(object, tuple):
+ return shortrepr(object, ')')
+ if isinstance(object, list):
+ return shortrepr(object, ']')
+ if isinstance(object, dict):
+ return shortrepr(object, '}')
+ module = inspect.getmodule(object)
+ if module is None:
+ return text_('object %s' % str(object))
+ modulename = module.__name__
+ if inspect.ismodule(object):
+ return text_('module %s' % modulename)
+ if inspect.ismethod(object):
+ oself = getattr(object, '__self__', None)
+ if oself is None: # pragma: no cover
+ oself = getattr(object, 'im_self', None)
+ return text_('method %s of class %s.%s' %
+ (object.__name__, modulename,
+ oself.__class__.__name__))
+
+ if inspect.isclass(object):
+ dottedname = '%s.%s' % (modulename, object.__name__)
+ return text_('class %s' % dottedname)
+ if inspect.isfunction(object):
+ dottedname = '%s.%s' % (modulename, object.__name__)
+ return text_('function %s' % dottedname)
+ return text_('object %s' % str(object))
+
+def shortrepr(object, closer):
+ r = str(object)
+ if len(r) > 100:
+ r = r[:100] + ' ... %s' % closer
+ return r