diff options
| author | Chris McDonough <chrism@plope.com> | 2011-12-04 19:27:46 -0500 |
|---|---|---|
| committer | Chris McDonough <chrism@plope.com> | 2011-12-04 19:27:46 -0500 |
| commit | 5d462f0c660f939c773862b0fab81728c9ba62c7 (patch) | |
| tree | 658df249d53de844c2f422ab04c4c26ee4feb86d | |
| parent | d5666e630a08c943a22682540aa51174cee6851f (diff) | |
| parent | a78b58dd5cf665f7a7aaa18e9e7f6cae3fc7f749 (diff) | |
| download | pyramid-5d462f0c660f939c773862b0fab81728c9ba62c7.tar.gz pyramid-5d462f0c660f939c773862b0fab81728c9ba62c7.tar.bz2 pyramid-5d462f0c660f939c773862b0fab81728c9ba62c7.zip | |
merge feature.introspection branch
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 --------- @@ -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 Binary files differnew file mode 100644 index 000000000..231a094f7 --- /dev/null +++ b/docs/narr/tb_introspector.png 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 |
