From 9264a759f7a4ce169818f3fbb1cda95d9f183645 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 14 Feb 2012 04:28:59 -0500 Subject: only mutate mapped_view if it's a function --- pyramid/config/views.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 1988b532b..b36bd1ca7 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -407,11 +407,15 @@ class DefaultViewMapper(object): mapped_view = self.map_nonclass_requestonly(view) elif self.attr: mapped_view = self.map_nonclass_attr(view) - if self.attr is not None: - mapped_view.__text__ = 'attr %s of %s' % ( - self.attr, object_description(view)) - else: - mapped_view.__text__ = object_description(view) + if inspect.isroutine(mapped_view): + # we potentially mutate an unwrapped view here if it's a function; + # we do this to avoid function call overhead of injecting another + # wrapper + if self.attr is not None: + mapped_view.__text__ = 'attr %s of %s' % ( + self.attr, object_description(view)) + else: + mapped_view.__text__ = object_description(view) return mapped_view def map_class_requestonly(self, view): -- cgit v1.2.3 From e45c8075bda2012fef878774812a683750406fff Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 14 Feb 2012 04:30:20 -0500 Subject: comment --- pyramid/config/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index b36bd1ca7..9d2e15537 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -72,6 +72,7 @@ def view_description(view): try: return view.__text__ except AttributeError: + # custom view mappers might not add __text__ return object_description(view) def wraps_view(wrapper): -- cgit v1.2.3 From 5ad401ad0b7692e55c21662416642b8ac0fde449 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 14 Feb 2012 05:23:25 -0500 Subject: - Better error message when a .pyc-only module is ``config.include`` -ed. This is not permitted due to error reporting requirements, and a better error message is shown when it is attempted. Previously it would fail with something like "AttributeError: 'NoneType' object has no attribute 'rfind'". Fixed #420. --- CHANGES.txt | 6 ++++++ pyramid/config/__init__.py | 11 +++++++++-- pyramid/tests/test_config/test_init.py | 20 ++++++++++++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 411681d81..6bea84e2f 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -17,6 +17,12 @@ Features The error message now contains information about the view callable itself as well as the result of calling it. +- Better error message when a .pyc-only module is ``config.include`` -ed. + This is not permitted due to error reporting requirements, and a better + error message is shown when it is attempted. Previously it would fail with + something like "AttributeError: 'NoneType' object has no attribute + 'rfind'". + Dependencies ------------ diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index 1656b5410..06d3c6abf 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -253,6 +253,7 @@ class Configurator( info = '' object_description = staticmethod(object_description) introspectable = Introspectable + inspect = inspect def __init__(self, registry=None, @@ -706,7 +707,7 @@ class Configurator( route_prefix = None c = self.maybe_dotted(callable) - module = inspect.getmodule(c) + module = self.inspect.getmodule(c) if module is c: try: c = getattr(module, 'includeme') @@ -716,7 +717,13 @@ class Configurator( ) spec = module.__name__ + ':' + c.__name__ - sourcefile = inspect.getsourcefile(c) + sourcefile = self.inspect.getsourcefile(c) + + if sourcefile is None: + raise ConfigurationError( + 'No source file for module %r (.py file must exist, ' + 'refusing to use orphan .pyc or .pyo file).' % module.__name__) + if action_state.processSpec(spec): configurator = self.__class__( diff --git a/pyramid/tests/test_config/test_init.py b/pyramid/tests/test_config/test_init.py index d237b3fe8..283800e1e 100644 --- a/pyramid/tests/test_config/test_init.py +++ b/pyramid/tests/test_config/test_init.py @@ -739,6 +739,26 @@ pyramid.tests.test_config.dummy_include2""", root_config.include(dummy_subapp, route_prefix='nested') + def test_include_with_missing_source_file(self): + from pyramid.exceptions import ConfigurationError + import inspect + config = self._makeOne() + class DummyInspect(object): + def getmodule(self, c): + return inspect.getmodule(c) + def getsourcefile(self, c): + return None + config.inspect = DummyInspect() + try: + config.include('pyramid.tests.test_config.dummy_include') + except ConfigurationError as e: + self.assertEqual( + e.args[0], + "No source file for module 'pyramid.tests.test_config' (.py " + "file must exist, refusing to use orphan .pyc or .pyo file).") + else: # pragma: no cover + raise AssertionError + def test_with_context(self): config = self._makeOne() context = DummyZCMLContext() -- cgit v1.2.3 From c595946b859e48ea506dd3155da935eba10a1ed7 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 14 Feb 2012 05:48:26 -0500 Subject: ignore venvs put into this dir --- docs/tutorials/.gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/tutorials/.gitignore diff --git a/docs/tutorials/.gitignore b/docs/tutorials/.gitignore new file mode 100644 index 000000000..71e689620 --- /dev/null +++ b/docs/tutorials/.gitignore @@ -0,0 +1 @@ +env*/ -- cgit v1.2.3 From c6a299ad7159ffcabe201fa79f485c388d837971 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 14 Feb 2012 05:57:06 -0500 Subject: - Don't create a ``session`` instance in SQLA Wiki tutorial, use raw ``DBSession`` instead (this is more common in real SQLA apps). --- CHANGES.txt | 6 ++++++ docs/tutorials/wiki2/authorization.rst | 4 ++-- docs/tutorials/wiki2/definingviews.rst | 8 ++++---- docs/tutorials/wiki2/src/authorization/tutorial/views.py | 13 +++++-------- docs/tutorials/wiki2/src/views/tutorial/views.py | 13 +++++-------- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 6bea84e2f..a69313a8a 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -23,6 +23,12 @@ Features something like "AttributeError: 'NoneType' object has no attribute 'rfind'". +Documentation +------------- + +- Don't create a ``session`` instance in SQLA Wiki tutorial, use raw + ``DBSession`` instead (this is more common in real SQLA apps). + Dependencies ------------ diff --git a/docs/tutorials/wiki2/authorization.rst b/docs/tutorials/wiki2/authorization.rst index 56237a1b9..b1d0bf37c 100644 --- a/docs/tutorials/wiki2/authorization.rst +++ b/docs/tutorials/wiki2/authorization.rst @@ -159,14 +159,14 @@ logged in user and redirect back to the front page. The ``login`` view callable will look something like this: .. literalinclude:: src/authorization/tutorial/views.py - :lines: 90-116 + :lines: 87-113 :linenos: :language: python The ``logout`` view callable will look something like this: .. literalinclude:: src/authorization/tutorial/views.py - :lines: 118-122 + :lines: 115-119 :linenos: :language: python diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst index bda0a2eb7..a067dbd66 100644 --- a/docs/tutorials/wiki2/definingviews.rst +++ b/docs/tutorials/wiki2/definingviews.rst @@ -126,7 +126,7 @@ HTML anchor for each *WikiWord* reference in the rendered HTML using a compiled regular expression. .. literalinclude:: src/views/tutorial/views.py - :lines: 23-44 + :lines: 23-43 :linenos: :language: python @@ -161,7 +161,7 @@ The ``matchdict`` attribute of the request passed to the ``add_page`` view will have the values we need to construct URLs and find model objects. .. literalinclude:: src/views/tutorial/views.py - :lines: 46-58 + :lines: 45-56 :linenos: :language: python @@ -184,7 +184,7 @@ If the view execution *is* a result of a form submission (if the expression ``'form.submitted' in request.params`` is ``True``), we scrape the page body from the form data, create a Page object with this page body and the name taken from ``matchdict['pagename']``, and save it into the database using -``session.add``. We then redirect back to the ``view_page`` view for the +``DBSession.add``. We then redirect back to the ``view_page`` view for the newly created page. The ``edit_page`` view function @@ -197,7 +197,7 @@ request passed to the ``edit_page`` view will have a ``'pagename'`` key matching the name of the page the user wants to edit. .. literalinclude:: src/views/tutorial/views.py - :lines: 60-73 + :lines: 58-70 :linenos: :language: python diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views.py b/docs/tutorials/wiki2/src/authorization/tutorial/views.py index 375f1f5a5..087e6076b 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/views.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/views.py @@ -33,14 +33,13 @@ def view_wiki(request): @view_config(route_name='view_page', renderer='templates/view.pt') def view_page(request): pagename = request.matchdict['pagename'] - session = DBSession() - page = session.query(Page).filter_by(name=pagename).first() + page = DBSession.query(Page).filter_by(name=pagename).first() if page is None: return HTTPNotFound('No such page') def check(match): word = match.group(1) - exists = session.query(Page).filter_by(name=word).all() + exists = DBSession.query(Page).filter_by(name=word).all() if exists: view_url = request.route_url('view_page', pagename=word) return '%s' % (view_url, word) @@ -59,10 +58,9 @@ def view_page(request): def add_page(request): name = request.matchdict['pagename'] if 'form.submitted' in request.params: - session = DBSession() body = request.params['body'] page = Page(name, body) - session.add(page) + DBSession.add(page) return HTTPFound(location = request.route_url('view_page', pagename=name)) save_url = request.route_url('add_page', pagename=name) @@ -74,11 +72,10 @@ def add_page(request): permission='edit') def edit_page(request): name = request.matchdict['pagename'] - session = DBSession() - page = session.query(Page).filter_by(name=name).one() + page = DBSession.query(Page).filter_by(name=name).one() if 'form.submitted' in request.params: page.data = request.params['body'] - session.add(page) + DBSession.add(page) return HTTPFound(location = request.route_url('view_page', pagename=name)) return dict( diff --git a/docs/tutorials/wiki2/src/views/tutorial/views.py b/docs/tutorials/wiki2/src/views/tutorial/views.py index 5c49dd2e8..c2a94a96b 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/views.py +++ b/docs/tutorials/wiki2/src/views/tutorial/views.py @@ -23,14 +23,13 @@ def view_wiki(request): @view_config(route_name='view_page', renderer='templates/view.pt') def view_page(request): pagename = request.matchdict['pagename'] - session = DBSession() - page = session.query(Page).filter_by(name=pagename).first() + page = DBSession.query(Page).filter_by(name=pagename).first() if page is None: return HTTPNotFound('No such page') def check(match): word = match.group(1) - exists = session.query(Page).filter_by(name=word).all() + exists = DBSession.query(Page).filter_by(name=word).all() if exists: view_url = request.route_url('view_page', pagename=word) return '%s' % (view_url, word) @@ -47,10 +46,9 @@ def view_page(request): def add_page(request): name = request.matchdict['pagename'] if 'form.submitted' in request.params: - session = DBSession() body = request.params['body'] page = Page(name, body) - session.add(page) + DBSession.add(page) return HTTPFound(location = request.route_url('view_page', pagename=name)) save_url = request.route_url('add_page', pagename=name) @@ -60,11 +58,10 @@ def add_page(request): @view_config(route_name='edit_page', renderer='templates/edit.pt') def edit_page(request): name = request.matchdict['pagename'] - session = DBSession() - page = session.query(Page).filter_by(name=name).one() + page = DBSession.query(Page).filter_by(name=name).one() if 'form.submitted' in request.params: page.data = request.params['body'] - session.add(page) + DBSession.add(page) return HTTPFound(location = request.route_url('view_page', pagename=name)) return dict( -- cgit v1.2.3 From 9bdb099a5f5cb9f70c1bc60c2e2f58d6bc6f5c28 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 14 Feb 2012 06:59:28 -0500 Subject: add note about debugtoolbar.hosts --- docs/narr/project.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/narr/project.rst b/docs/narr/project.rst index d69f0cf13..66643af73 100644 --- a/docs/narr/project.rst +++ b/docs/narr/project.rst @@ -322,6 +322,22 @@ image again. .. image:: project-debug.png +If you don't see the debug toolbar image on the right hand top of the page, +it means you're browsing from a system that does not have debugging access. +By default, for security reasons, only a browser originating from +``localhost`` (``127.0.0.1``) can see the debug toolbar. To allow your +browser on a remote system to access the server, add the a line within the +``[app:main]`` section of the ``development.ini`` file in the form +``debugtoolbar.hosts = X.X.X.X``. For example, if your Pyramid application +is running on a remote system, and you're browsing from a host with the IP +address ``192.168.1.1``, you'd add something like this to enable the toolbar +when your system contacts Pyramid: + +.. code-block:: ini + + [app:main] + debugtoolbar.hosts = 192.168.1.1 + For more information about what the debug toolbar allows you to do, see `the documentation for pyramid_debugtoolbar `_. -- cgit v1.2.3 From 37f3baaa637ee6184eac5eff31ca787072f0cc2c Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 14 Feb 2012 06:59:37 -0500 Subject: garden --- TODO.txt | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/TODO.txt b/TODO.txt index 3d11470dd..f1376622d 100644 --- a/TODO.txt +++ b/TODO.txt @@ -7,8 +7,6 @@ Must-Have - Fix deployment recipes in cookbook (discourage proxying without changing server). -- Use waitress instead of wsgiref. - - pyramid.config.util.ActionInfo.__str__ potentially returns Unicode under Py2, fix. @@ -17,6 +15,8 @@ Must-Have Nice-to-Have ------------ +- Modify view mapper narrative docs to not use pyramid_handlers. + - Modify the urldispatch chapter examples to assume a scan rather than ``add_view``. @@ -38,18 +38,10 @@ Nice-to-Have - Fix deployment recipes in cookbook (discourage proxying without changing server). -Nice-to-Have ------------- - -- CherryPy server testing / exploded from CherryPy itself. - - Try "with transaction.manager" in an exception view with SQLA (preempt homina homina response about how to write "to the database" from within in an exception view). -- Add a default-view-config-params decorator that can be applied to a class - which names defaults for method-based view_config decorator options. - - Add narrative docs for wsgiapp and wsgiapp2. - Flesh out "Paste" narrative docs chapter. -- cgit v1.2.3 From 72bb7174bb4a4c4547828ab20d3b174b35200e47 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 14 Feb 2012 07:00:39 -0500 Subject: denote other settings --- docs/narr/project.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/narr/project.rst b/docs/narr/project.rst index 66643af73..d626667ca 100644 --- a/docs/narr/project.rst +++ b/docs/narr/project.rst @@ -336,6 +336,7 @@ when your system contacts Pyramid: .. code-block:: ini [app:main] + # .. other settings ... debugtoolbar.hosts = 192.168.1.1 For more information about what the debug toolbar allows you to do, see `the -- cgit v1.2.3 From b227b60f5572767aefd0165d7cfc0a58cf546dd7 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 14 Feb 2012 07:02:59 -0500 Subject: garden --- TODO.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/TODO.txt b/TODO.txt index f1376622d..1d0dbd620 100644 --- a/TODO.txt +++ b/TODO.txt @@ -15,6 +15,10 @@ Must-Have Nice-to-Have ------------ +- Put includes in development.ini on separate lines and fix project.rst to + tell people to comment out only the debugtoolbar include when they want to + disable. + - Modify view mapper narrative docs to not use pyramid_handlers. - Modify the urldispatch chapter examples to assume a scan rather than -- cgit v1.2.3 From 2f35a1e938b7f4e9ad182f1ead415632769345d6 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 14 Feb 2012 07:10:43 -0500 Subject: use the newer url --- docs/narr/project.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/project.rst b/docs/narr/project.rst index d626667ca..4566a4fb8 100644 --- a/docs/narr/project.rst +++ b/docs/narr/project.rst @@ -341,7 +341,7 @@ when your system contacts Pyramid: For more information about what the debug toolbar allows you to do, see `the documentation for pyramid_debugtoolbar -`_. +`_. The debug toolbar will not be shown (and all debugging will be turned off) when you use the ``production.ini`` file instead of the ``development.ini`` -- cgit v1.2.3 From 2d5aa5b0e1a2abea9efcb99e8305bea305723130 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 15 Feb 2012 16:17:57 -0500 Subject: garden --- TODO.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/TODO.txt b/TODO.txt index 1d0dbd620..05022cd86 100644 --- a/TODO.txt +++ b/TODO.txt @@ -15,6 +15,9 @@ Must-Have Nice-to-Have ------------ +- Replace all mentions of zope.interface.implements with + zope.interface.implementer. + - Put includes in development.ini on separate lines and fix project.rst to tell people to comment out only the debugtoolbar include when they want to disable. -- cgit v1.2.3 From 2e7f0cfb4c5f0a4804e0c44cdf181c2ee35b020a Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 15 Feb 2012 16:18:06 -0500 Subject: remove unused imports --- docs/narr/renderers.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/narr/renderers.rst b/docs/narr/renderers.rst index ed391f4fe..67354a1ed 100644 --- a/docs/narr/renderers.rst +++ b/docs/narr/renderers.rst @@ -156,7 +156,6 @@ dictionary: .. code-block:: python :linenos: - from pyramid.response import Response from pyramid.view import view_config @view_config(renderer='string') @@ -193,7 +192,6 @@ render the returned dictionary to a JSON serialization: .. code-block:: python :linenos: - from pyramid.response import Response from pyramid.view import view_config @view_config(renderer='json') -- cgit v1.2.3 From 1ca5b34f17ea889f705c1120a5b8878bff16ec63 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 15 Feb 2012 16:23:59 -0500 Subject: - Replace all mentions of zope.interface.implements with zope.interface.implementer. --- TODO.txt | 3 --- docs/narr/hooks.rst | 15 ++++++++------- docs/narr/resources.rst | 10 +++++----- docs/narr/traversal.rst | 9 +++++---- 4 files changed, 18 insertions(+), 19 deletions(-) diff --git a/TODO.txt b/TODO.txt index 05022cd86..1d0dbd620 100644 --- a/TODO.txt +++ b/TODO.txt @@ -15,9 +15,6 @@ Must-Have Nice-to-Have ------------ -- Replace all mentions of zope.interface.implements with - zope.interface.implementer. - - Put includes in development.ini on separate lines and fix project.rst to tell people to comment out only the debugtoolbar include when they want to disable. diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index fd6544416..350b5734d 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -606,24 +606,24 @@ adapter to the more complex IResponse interface: If you want to implement your own Response object instead of using the :class:`pyramid.response.Response` object in any capacity at all, you'll have to make sure the object implements every attribute and method outlined in -:class:`pyramid.interfaces.IResponse` and you'll have to ensure that it's -marked up with ``zope.interface.implements(IResponse)``: +:class:`pyramid.interfaces.IResponse` and you'll have to ensure that it uses +``zope.interface.implementer(IResponse)`` as a class decoratoror. .. code-block:: python :linenos: from pyramid.interfaces import IResponse - from zope.interface import implements + from zope.interface import implementer + @implementer(IResponse) class MyResponse(object): - implements(IResponse) # ... an implementation of every method and attribute # documented in IResponse should follow ... When an alternate response object implementation is returned by a view callable, if that object asserts that it implements :class:`~pyramid.interfaces.IResponse` (via -``zope.interface.implements(IResponse)``) , an adapter needn't be registered +``zope.interface.implementer(IResponse)``) , an adapter needn't be registered for the object; Pyramid will use it directly. An IResponse adapter for ``webob.Response`` (as opposed to @@ -812,14 +812,15 @@ performed, enabling you to set up the utility in advance: .. code-block:: python :linenos: + from zope.interface import implementer + from wsgiref.simple_server import make_server from pyramid.config import Configurator from mypackage.interfaces import IMyUtility + @implementer(IMyUtility) class UtilityImplementation: - implements(IMyUtility) - def __init__(self): self.registrations = {} diff --git a/docs/narr/resources.rst b/docs/narr/resources.rst index 256f69fc3..83734be9f 100644 --- a/docs/narr/resources.rst +++ b/docs/narr/resources.rst @@ -540,14 +540,14 @@ declares that the blog entry implements an :term:`interface`. :linenos: import datetime - from zope.interface import implements + from zope.interface import implementer from zope.interface import Interface class IBlogEntry(Interface): pass + @implementer(IBlogEntry) class BlogEntry(object): - implements(IBlogEntry) def __init__(self, title, body, author): self.title = title self.body = body @@ -556,15 +556,15 @@ declares that the blog entry implements an :term:`interface`. This resource consists of two things: the class which defines the resource constructor as the class ``BlogEntry``, and an :term:`interface` attached to -the class via an ``implements`` statement at class scope using the -``IBlogEntry`` interface as its sole argument. +the class via an ``implementer`` class decorator using the ``IBlogEntry`` +interface as its sole argument. The interface object used must be an instance of a class that inherits from :class:`zope.interface.Interface`. A resource class may implement zero or more interfaces. You specify that a resource implements an interface by using the -:func:`zope.interface.implements` function at class scope. The above +:func:`zope.interface.implementer` function as a class decorator. The above ``BlogEntry`` resource implements the ``IBlogEntry`` interface. You can also specify that a particular resource *instance* provides an diff --git a/docs/narr/traversal.rst b/docs/narr/traversal.rst index 8c5d950c1..8e7f93a1b 100644 --- a/docs/narr/traversal.rst +++ b/docs/narr/traversal.rst @@ -488,20 +488,21 @@ you must create an interface and mark up your resource classes or instances with interface declarations that refer to this interface. To attach an interface to a resource *class*, you define the interface and -use the :func:`zope.interface.implements` function to associate the interface -with the class. +use the :func:`zope.interface.implementer` class decorator to associate the +interface with the class. .. code-block:: python :linenos: from zope.interface import Interface - from zope.interface import implements + from zope.interface import implementer class IHello(Interface): """ A marker interface """ + @implementer(IHello) class Hello(object): - implements(IHello) + pass To attach an interface to a resource *instance*, you define the interface and use the :func:`zope.interface.alsoProvides` function to associate the -- cgit v1.2.3 From 157b1e4c70d3aea96c1fdaede13a48f9235fe779 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 15 Feb 2012 17:13:51 -0500 Subject: garden --- TODO.txt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/TODO.txt b/TODO.txt index 1d0dbd620..258e4912c 100644 --- a/TODO.txt +++ b/TODO.txt @@ -10,11 +10,12 @@ Must-Have - pyramid.config.util.ActionInfo.__str__ potentially returns Unicode under Py2, fix. -- Tests for view names/route patterns that contain Unicode. - Nice-to-Have ------------ +- Add set_traverser configurator method and set_resource_url_generator + method. + - Put includes in development.ini on separate lines and fix project.rst to tell people to comment out only the debugtoolbar include when they want to disable. -- cgit v1.2.3 From 11f2b29e749456b710863b989da1f1b1fa5edebb Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 15 Feb 2012 17:18:00 -0500 Subject: garden --- TODO.txt | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/TODO.txt b/TODO.txt index 258e4912c..75dd2e0c4 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,15 +1,6 @@ Pyramid TODOs ============= -Must-Have ---------- - -- Fix deployment recipes in cookbook (discourage proxying without changing - server). - -- pyramid.config.util.ActionInfo.__str__ potentially returns Unicode under - Py2, fix. - Nice-to-Have ------------ @@ -25,7 +16,7 @@ Nice-to-Have - Modify the urldispatch chapter examples to assume a scan rather than ``add_view``. -- Decorator for append_slash_notfound_view_factory? +- Decorator for append_slash_notfound_view_factory. - Introspection: -- cgit v1.2.3 From c0b7076cb5ad4dcf5840c95f527955e3483d7cae Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 15 Feb 2012 17:36:47 -0500 Subject: handhold more after rendering the sqla scaffold --- pyramid/scaffolds/__init__.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/pyramid/scaffolds/__init__.py b/pyramid/scaffolds/__init__.py index ab2b3034a..ad5753713 100644 --- a/pyramid/scaffolds/__init__.py +++ b/pyramid/scaffolds/__init__.py @@ -1,5 +1,6 @@ import binascii import os +import sys from pyramid.compat import native_ @@ -52,10 +53,14 @@ class AlchemyProjectTemplate(PyramidTemplate): summary = 'Pyramid SQLAlchemy project using url dispatch' def post(self, command, output_dir, vars): # pragma: no cover val = PyramidTemplate.post(self, command, output_dir, vars) + vars = vars.copy() + vars['output_dir'] = output_dir + vars['pybin'] = os.path.join(sys.exec_prefix, 'bin') self.out('') self.out('Please run the "populate_%(project)s" script to set up the ' - 'SQL database after installing (but before starting) the ' - 'application (e.g. ' - '"$myvirtualenv/bin/populate_%(project)s development.ini".)' + 'SQL database after\ninstalling (but before starting) the ' + 'application.\n\n For example:\n\ncd %(output_dir)s\n' + '%(pybin)s/python setup.py develop\n' + '%(pybin)s/populate_%(project)s development.ini' % vars) return val -- cgit v1.2.3 From 748aad47f90136b151be13f477ed6af1caed0493 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 15 Feb 2012 19:09:08 -0500 Subject: - Add ``pyramid.config.Configurator.set_traverser`` API method. See the Hooks narrative documentation section entitled "Changing the Traverser" for more information. This is not a new feature, it just provides an API for adding a traverser without needing to use the ZCA API. --- CHANGES.txt | 5 ++ TODO.txt | 3 +- docs/api/config.rst | 2 + docs/narr/hooks.rst | 14 +++--- docs/narr/introspector.rst | 15 ++++++ pyramid/config/factories.py | 74 +++++++++++++++++++++++++++++ pyramid/tests/test_config/test_factories.py | 47 ++++++++++++++++++ 7 files changed, 150 insertions(+), 10 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index a69313a8a..0bb2c164b 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -23,6 +23,11 @@ Features something like "AttributeError: 'NoneType' object has no attribute 'rfind'". +- Add ``pyramid.config.Configurator.set_traverser`` API method. See the + Hooks narrative documentation section entitled "Changing the Traverser" for + more information. This is not a new feature, it just provides an API for + adding a traverser without needing to use the ZCA API. + Documentation ------------- diff --git a/TODO.txt b/TODO.txt index 75dd2e0c4..dfce2e2fb 100644 --- a/TODO.txt +++ b/TODO.txt @@ -4,8 +4,7 @@ Pyramid TODOs Nice-to-Have ------------ -- Add set_traverser configurator method and set_resource_url_generator - method. +- Add set_resource_url_generator method. - Put includes in development.ini on separate lines and fix project.rst to tell people to comment out only the debugtoolbar include when they want to diff --git a/docs/api/config.rst b/docs/api/config.rst index d16930cc0..3c5ee563a 100644 --- a/docs/api/config.rst +++ b/docs/api/config.rst @@ -94,6 +94,8 @@ .. automethod:: set_notfound_view + .. automethod:: set_traverser + .. automethod:: set_renderer_globals_factory(factory) .. attribute:: introspectable diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 350b5734d..076f9fa5c 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -406,11 +406,10 @@ via configuration. .. code-block:: python :linenos: - from pyramid.interfaces import ITraverser - from zope.interface import Interface + from pyramid.config import Configurator from myapp.traversal import Traverser - - config.registry.registerAdapter(Traverser, (Interface,), ITraverser) + config = Configurator() + config.set_traverser(Traverser) In the example above, ``myapp.traversal.Traverser`` is assumed to be a class that implements the following interface: @@ -456,12 +455,11 @@ used. Otherwise, the default traverser would be used. For example: .. code-block:: python :linenos: - from pyramid.interfaces import ITraverser - from zope.interface import Interface from myapp.traversal import Traverser from myapp.resources import MyRoot - - config.registry.registerAdapter(Traverser, (MyRoot,), ITraverser) + from pyramid.config import Configurator + config = Configurator() + config.set_traverser(Traverser, MyRoot) If the above stanza was added to a Pyramid ``__init__.py`` file's ``main`` function, :app:`Pyramid` would use the ``myapp.traversal.Traverser`` only diff --git a/docs/narr/introspector.rst b/docs/narr/introspector.rst index 11d779854..08cc430f6 100644 --- a/docs/narr/introspector.rst +++ b/docs/narr/introspector.rst @@ -529,6 +529,21 @@ introspectables in categories not described here. A normalized version of the ``spec`` argument provided to ``add_static_view``. +``traversers`` + + Each introspectable in the ``traversers`` category represents a call to + :meth:`pyramid.config.Configurator.add_traverser`; each will have the + following data. + + ``iface`` + + The (resolved) interface or class object that represents the return value + of a root factory that this traverser will be used for. + + ``factory`` + + The (resolved) traverser class. + Introspection in the Toolbar ---------------------------- diff --git a/pyramid/config/factories.py b/pyramid/config/factories.py index eb4442e98..7c0ea054d 100644 --- a/pyramid/config/factories.py +++ b/pyramid/config/factories.py @@ -1,3 +1,5 @@ +from zope.interface import Interface + from pyramid.config.util import action_method from pyramid.interfaces import ( @@ -7,6 +9,7 @@ from pyramid.interfaces import ( IRequestProperties, IRootFactory, ISessionFactory, + ITraverser, ) from pyramid.traversal import DefaultRootFactory @@ -140,6 +143,77 @@ class FactoriesConfiguratorMixin(object): self.action(('request properties', name), register, introspectables=(intr,)) + def set_traverser(self, factory, iface=None): + """ + The superdefault :term:`traversal` algorithm that :app:`Pyramid` uses + is explained in :ref:`traversal_algorithm`. Though it is rarely + necessary, this default algorithm can be swapped out selectively for + a different traversal pattern via configuration. The section + entitled :ref:`changing_the_traverser` details how to create a + traverser class. + + For example, to override the superdefault traverser used by Pyramid, + you might do something like this: + + .. code-block:: python + + from myapp.traversal import MyCustomTraverser + config.set_traverser(MyCustomTraverser) + + This would cause the Pyramid superdefault traverser to never be used; + intead all traversal would be done using your ``MyCustomTraverser`` + class, no matter which object was returned by the :term:`root + factory` of this application. Note that we passed no arguments to + the ``iface`` keyword parameter. The default value of ``iface``, + ``None`` represents that the registered traverser should be used when + no other more specific traverser is available for the object returned + by the root factory. + + However, more than one traversal algorithm can be active at the same + time. The traverser used can depend on the result of the :term:`root + factory`. For instance, if your root factory returns more than one + type of object conditionally, you could claim that an alternate + traverser adapter should be used agsinst one particular class or + interface returned by that root factory. When the root factory + returned an object that implemented that class or interface, a custom + traverser would be used. Otherwise, the default traverser would be + used. The ``iface`` argument represents the class of the object that + the root factory might return or an :term:`interface` that the object + might implement. + + To use a particular traverser only when the root factory returns a + particular class: + + .. code-block:: python + + config.set_traverser(MyCustomTraverser, MyRootClass) + + When more than one traverser is active, the "most specific" traverser + will be used (the one that matches the class or interface of the + value returned by the root factory most closely). + + Note that either ``factory`` or ``iface`` can be a :term:`dotted + Python name` or a Python object. + + See :ref:`changing_the_traverser` for more information. + """ + iface = self.maybe_dotted(iface) + factory = self.maybe_dotted(factory) + def register(iface=iface): + if iface is None: + iface = Interface + self.registry.registerAdapter(factory, (iface,), ITraverser) + discriminator = ('traverser', iface) + intr = self.introspectable( + 'traversers', + discriminator, + 'traverser for %r' % iface, + 'traverser', + ) + intr['factory'] = factory + intr['iface'] = iface + self.action(('traverser', iface), register, introspectables=(intr,)) + def _set_request_properties(event): request = event.request plist = request.registry.queryUtility(IRequestProperties) diff --git a/pyramid/tests/test_config/test_factories.py b/pyramid/tests/test_config/test_factories.py index d1a01568f..51c60896e 100644 --- a/pyramid/tests/test_config/test_factories.py +++ b/pyramid/tests/test_config/test_factories.py @@ -129,6 +129,46 @@ class TestFactoriesMixin(unittest.TestCase): self.assertEqual(callables, [('foo', foo, False), ('bar', foo, True)]) + def test_set_traverser_dotted_names(self): + from pyramid.interfaces import ITraverser + config = self._makeOne(autocommit=True) + config.set_traverser( + 'pyramid.tests.test_config.test_factories.DummyTraverser', + 'pyramid.tests.test_config.test_factories.DummyIface') + iface = DummyIface() + traverser = config.registry.getAdapter(iface, ITraverser) + self.assertEqual(traverser.__class__, DummyTraverser) + self.assertEqual(traverser.root, iface) + + def test_set_traverser_default_iface_means_Interface(self): + from pyramid.interfaces import ITraverser + config = self._makeOne(autocommit=True) + config.set_traverser(DummyTraverser) + traverser = config.registry.getAdapter(None, ITraverser) + self.assertEqual(traverser.__class__, DummyTraverser) + + def test_set_traverser_nondefault_iface(self): + from pyramid.interfaces import ITraverser + config = self._makeOne(autocommit=True) + config.set_traverser(DummyTraverser, DummyIface) + iface = DummyIface() + traverser = config.registry.getAdapter(iface, ITraverser) + self.assertEqual(traverser.__class__, DummyTraverser) + self.assertEqual(traverser.root, iface) + + def test_set_traverser_introspectables(self): + config = self._makeOne() + config.set_traverser(DummyTraverser, DummyIface) + actions = config.action_state.actions + self.assertEqual(len(actions), 1) + intrs = actions[0]['introspectables'] + self.assertEqual(len(intrs), 1) + intr = intrs[0] + self.assertEqual(intr.type_name, 'traverser') + self.assertEqual(intr.discriminator, ('traverser', DummyIface)) + self.assertEqual(intr.category_name, 'traversers') + self.assertEqual(intr.title, 'traverser for %r' % DummyIface) + class DummyRequest(object): callables = None @@ -139,3 +179,10 @@ class DummyRequest(object): if self.callables is None: self.callables = [] self.callables.append((name, callable, reify)) + +class DummyTraverser(object): + def __init__(self, root): + self.root = root + +class DummyIface(object): + pass -- cgit v1.2.3 From f9bcf47164a151587244deb3ce5334ba447e3b99 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 15 Feb 2012 19:13:58 -0500 Subject: update whatsnew --- docs/whatsnew-1.3.rst | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docs/whatsnew-1.3.rst b/docs/whatsnew-1.3.rst index b6cfde039..918870018 100644 --- a/docs/whatsnew-1.3.rst +++ b/docs/whatsnew-1.3.rst @@ -259,6 +259,30 @@ Minor Feature Additions http://readthedocs.org/docs/venusian/en/latest/#ignore-scan-argument for more information about how to use the ``ignore`` argument to ``scan``. +- Add :meth:`pyramid.config.Configurator.set_traverser` API method. See + :ref:`changing_the_traverser` for more information. This is not a new + feature, it just provides an API for adding a traverser without needing to + use the ZCA API. + +- The :meth:`pyramid.config.Configurator.scan` method can now be passed an + ``ignore`` argument, which can be a string, a callable, or a list + consisting of strings and/or callables. This feature allows submodules, + subpackages, and global objects from being scanned. See + http://readthedocs.org/docs/venusian/en/latest/#ignore-scan-argument for + more information about how to use the ``ignore`` argument to ``scan``. + +- Better error messages when a view callable returns a value that cannot be + converted to a response (for example, when a view callable returns a + dictionary without a renderer defined, or doesn't return any value at all). + The error message now contains information about the view callable itself + as well as the result of calling it. + +- Better error message when a .pyc-only module is ``config.include`` -ed. + This is not permitted due to error reporting requirements, and a better + error message is shown when it is attempted. Previously it would fail with + something like "AttributeError: 'NoneType' object has no attribute + 'rfind'". + Backwards Incompatibilities --------------------------- @@ -375,6 +399,8 @@ Dependency Changes - Pyramid no longer depends on the ``Paste`` or ``PasteScript`` packages. These packages are not Python 3 compatible. +- Depend on ``venusian`` >= 1.0a3 to provide scan ``ignore`` support. + Scaffolding Changes ------------------- -- cgit v1.2.3 From 4786cae2e7a053b01091b5e185102f9c26885b08 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 15 Feb 2012 19:32:46 -0500 Subject: - The system value ``r`` is now supplied to renderers as an alias for ``request``. This means that you can now, for example, in a template, do ``r.route_url(...)`` instead of ``request.route_url(...)``. Fixes #413. --- CHANGES.txt | 4 ++++ TODO.txt | 3 +++ docs/narr/renderers.rst | 18 +++++++++--------- docs/whatsnew-1.3.rst | 7 ++++++- pyramid/renderers.py | 4 +++- pyramid/tests/test_renderers.py | 6 ++++-- 6 files changed, 29 insertions(+), 13 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 0bb2c164b..d9106ddb9 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -28,6 +28,10 @@ Features more information. This is not a new feature, it just provides an API for adding a traverser without needing to use the ZCA API. +- The system value ``r`` is now supplied to renderers as an alias for + ``request``. This means that you can now, for example, in a template, do + ``r.route_url(...)`` instead of ``request.route_url(...)``. + Documentation ------------- diff --git a/TODO.txt b/TODO.txt index dfce2e2fb..8ea5e3591 100644 --- a/TODO.txt +++ b/TODO.txt @@ -4,6 +4,9 @@ Pyramid TODOs Nice-to-Have ------------ +- Fix renderers chapter to better document system values passed to template + renderers. + - Add set_resource_url_generator method. - Put includes in development.ini on separate lines and fix project.rst to diff --git a/docs/narr/renderers.rst b/docs/narr/renderers.rst index 67354a1ed..1622f1f29 100644 --- a/docs/narr/renderers.rst +++ b/docs/narr/renderers.rst @@ -333,15 +333,15 @@ dictionary, an error will be raised. Before passing keywords to the template, the keyword arguments derived from the dictionary returned by the view are augmented. The callable object -- -whatever object was used to define the view -- will be automatically -inserted into the set of keyword arguments passed to the template as the -``view`` keyword. If the view callable was a class, the ``view`` keyword -will be an instance of that class. Also inserted into the keywords passed to -the template are ``renderer_name`` (the string used in the ``renderer`` -attribute of the directive), ``renderer_info`` (an object containing -renderer-related information), ``context`` (the context resource of the view -used to render the template), and ``request`` (the request passed to the view -used to render the template). +whatever object was used to define the view -- will be automatically inserted +into the set of keyword arguments passed to the template as the ``view`` +keyword. If the view callable was a class, the ``view`` keyword will be an +instance of that class. Also inserted into the keywords passed to the +template are ``renderer_name`` (the string used in the ``renderer`` attribute +of the directive), ``renderer_info`` (an object containing renderer-related +information), ``context`` (the context resource of the view used to render +the template), and ``request`` (the request passed to the view used to render +the template). ``request`` is also available as ``r`` in Pyramid 1.3+. Here's an example view configuration which uses a Chameleon ZPT renderer: diff --git a/docs/whatsnew-1.3.rst b/docs/whatsnew-1.3.rst index 918870018..c4bde2b54 100644 --- a/docs/whatsnew-1.3.rst +++ b/docs/whatsnew-1.3.rst @@ -54,7 +54,8 @@ to make some changes: - We've replaced the ``paster`` command with Pyramid-specific analogues. -- We've made the default WSGI server the ``waitress`` server. +- We've made the default WSGI server used by Pyramid scaffolding the + :term:`waitress` server. Previously (in Pyramid 1.0, 1.1 and 1.2), you created a Pyramid application using ``paster create``, like so:: @@ -283,6 +284,10 @@ Minor Feature Additions something like "AttributeError: 'NoneType' object has no attribute 'rfind'". +- The system value ``r`` is now supplied to renderers as an alias for + ``request``. This means that you can now, for example, in a template, do + ``r.route_url(...)`` instead of ``request.route_url(...)``. + Backwards Incompatibilities --------------------------- diff --git a/pyramid/renderers.py b/pyramid/renderers.py index 61f5e0b35..a94d86e20 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -390,7 +390,8 @@ class RendererHelper(object): 'renderer_name':self.name, # b/c 'renderer_info':self, 'context':context, - 'request':request + 'request':request, + 'r':request, } return self.render_to_response(response, system, request=request) @@ -403,6 +404,7 @@ class RendererHelper(object): 'renderer_info':self, 'context':getattr(request, 'context', None), 'request':request, + 'r':request, } system_values = BeforeRender(system_values, value) diff --git a/pyramid/tests/test_renderers.py b/pyramid/tests/test_renderers.py index dbdfb06b3..2c27c6c29 100644 --- a/pyramid/tests/test_renderers.py +++ b/pyramid/tests/test_renderers.py @@ -484,7 +484,8 @@ class TestRendererHelper(unittest.TestCase): 'renderer_name': 'loo.foo', 'request': request, 'context': 'context', - 'view': 'view'} + 'view': 'view', + 'r': request,} ) def test_render_explicit_registry(self): @@ -517,7 +518,8 @@ class TestRendererHelper(unittest.TestCase): 'context':context, 'renderer_name':'loo.foo', 'view':None, - 'renderer_info':helper + 'renderer_info':helper, + 'r':request, } self.assertEqual(result[0], 'values') self.assertEqual(result[1], system) -- cgit v1.2.3 From 2a75f355dcf60ce1c8f537b4dfa35dbc15554661 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 15 Feb 2012 19:33:45 -0500 Subject: add sentence --- CHANGES.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index d9106ddb9..93ba8a199 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -30,7 +30,9 @@ Features - The system value ``r`` is now supplied to renderers as an alias for ``request``. This means that you can now, for example, in a template, do - ``r.route_url(...)`` instead of ``request.route_url(...)``. + ``r.route_url(...)`` instead of ``request.route_url(...)``. This is purely + a change to reduce the amount of typing required to reference request + methods from within templates. Documentation ------------- -- cgit v1.2.3 From b2ea4c88b8b3bc9ed657160d8a888780d6c41844 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 15 Feb 2012 22:06:30 -0500 Subject: Use req instead of r for #413. It's more likely that somebody is already passing something named r, and a template may depend on its existence or nonexistence to conditionalize rendering a bit of html. --- CHANGES.txt | 9 +++++---- docs/narr/renderers.rst | 2 +- docs/whatsnew-1.3.rst | 7 +++++-- pyramid/renderers.py | 4 ++-- pyramid/tests/test_renderers.py | 4 ++-- 5 files changed, 15 insertions(+), 11 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 93ba8a199..26d547ae6 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -28,11 +28,12 @@ Features more information. This is not a new feature, it just provides an API for adding a traverser without needing to use the ZCA API. -- The system value ``r`` is now supplied to renderers as an alias for +- The system value ``req`` is now supplied to renderers as an alias for ``request``. This means that you can now, for example, in a template, do - ``r.route_url(...)`` instead of ``request.route_url(...)``. This is purely - a change to reduce the amount of typing required to reference request - methods from within templates. + ``req.route_url(...)`` instead of ``request.route_url(...)``. This is + purely a change to reduce the amount of typing required to use request + methods and attributes from within templates. The value ``request`` is + still available too, this is just an alternative. Documentation ------------- diff --git a/docs/narr/renderers.rst b/docs/narr/renderers.rst index 1622f1f29..1f1b1943b 100644 --- a/docs/narr/renderers.rst +++ b/docs/narr/renderers.rst @@ -341,7 +341,7 @@ template are ``renderer_name`` (the string used in the ``renderer`` attribute of the directive), ``renderer_info`` (an object containing renderer-related information), ``context`` (the context resource of the view used to render the template), and ``request`` (the request passed to the view used to render -the template). ``request`` is also available as ``r`` in Pyramid 1.3+. +the template). ``request`` is also available as ``req`` in Pyramid 1.3+. Here's an example view configuration which uses a Chameleon ZPT renderer: diff --git a/docs/whatsnew-1.3.rst b/docs/whatsnew-1.3.rst index c4bde2b54..a27ef6af9 100644 --- a/docs/whatsnew-1.3.rst +++ b/docs/whatsnew-1.3.rst @@ -284,9 +284,12 @@ Minor Feature Additions something like "AttributeError: 'NoneType' object has no attribute 'rfind'". -- The system value ``r`` is now supplied to renderers as an alias for +- The system value ``req`` is now supplied to renderers as an alias for ``request``. This means that you can now, for example, in a template, do - ``r.route_url(...)`` instead of ``request.route_url(...)``. + ``req.route_url(...)`` instead of ``request.route_url(...)``. This is + purely a change to reduce the amount of typing required to use request + methods and attributes from within templates. The value ``request`` is + still available too, this is just an alternative. Backwards Incompatibilities --------------------------- diff --git a/pyramid/renderers.py b/pyramid/renderers.py index a94d86e20..14941c61a 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -391,7 +391,7 @@ class RendererHelper(object): 'renderer_info':self, 'context':context, 'request':request, - 'r':request, + 'req':request, } return self.render_to_response(response, system, request=request) @@ -404,7 +404,7 @@ class RendererHelper(object): 'renderer_info':self, 'context':getattr(request, 'context', None), 'request':request, - 'r':request, + 'req':request, } system_values = BeforeRender(system_values, value) diff --git a/pyramid/tests/test_renderers.py b/pyramid/tests/test_renderers.py index 2c27c6c29..b32e68e25 100644 --- a/pyramid/tests/test_renderers.py +++ b/pyramid/tests/test_renderers.py @@ -485,7 +485,7 @@ class TestRendererHelper(unittest.TestCase): 'request': request, 'context': 'context', 'view': 'view', - 'r': request,} + 'req': request,} ) def test_render_explicit_registry(self): @@ -519,7 +519,7 @@ class TestRendererHelper(unittest.TestCase): 'renderer_name':'loo.foo', 'view':None, 'renderer_info':helper, - 'r':request, + 'req':request, } self.assertEqual(result[0], 'values') self.assertEqual(result[1], system) -- cgit v1.2.3 From c51896756eeffc7e8c50ad71300ec355ae47465a Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 17 Feb 2012 01:08:42 -0500 Subject: Features -------- - Add ``pyramid.config.Configurator.add_resource_url_adapter`` API method. See the Hooks narrative documentation section entitled "Changing How pyramid.request.Request.resource_url Generates a URL" for more information. This is not a new feature, it just provides an API for adding a resource url adapter without needing to use the ZCA API. - A new interface was added: ``pyramid.interfaces.IResourceURL``. An adapter implementing its interface can be used to override resource URL generation when ``request.resource_url`` is called. This interface replaces the now-deprecated ``pyramid.interfaces.IContextURL`` interface. - The dictionary passed to a resource's ``__resource_url__`` method (see "Overriding Resource URL Generation" in the "Resources" chapter) now contains an ``app_url`` key, representing the application URL generated during ``request.resource_url``. It represents a potentially customized URL prefix, containing potentially custom scheme, host and port information passed by the user to ``request.resource_url``. It should be used instead of ``request.application_url`` where necessary. - The ``request.resource_url`` API now accepts these arguments: ``app_url``, ``scheme``, ``host``, and ``port``. The app_url argument can be used to replace the URL prefix wholesale during url generation. The ``scheme``, ``host``, and ``port`` arguments can be used to replace the respective default values of ``request.application_url`` partially. - A new API named ``request.resource_path`` now exists. It works like ``request.resource_url`` but produces a relative URL rather than an absolute one. - The ``request.route_url`` API now accepts these arguments: ``_app_url``, ``_scheme``, ``_host``, and ``_port``. The ``_app_url`` argument can be used to replace the URL prefix wholesale during url generation. The ``_scheme``, ``_host``, and ``_port`` arguments can be used to replace the respective default values of ``request.application_url`` partially. Backwards Incompatibilities --------------------------- - The ``pyramid.interfaces.IContextURL`` interface has been deprecated. People have been instructed to use this to register a resource url adapter in the "Hooks" chapter to use to influence ``request.resource_url`` URL generation for resources found via custom traversers since Pyramid 1.0. The interface still exists and registering such an adapter still works, but this interface will be removed from the software after a few major Pyramid releases. You should replace it with an equivalent ``pyramid.interfaces.IResourceURL`` adapter, registered using the new ``pyramid.config.Configurator.add_resource_url_adapter`` API. A deprecation warning is now emitted when a ``pyramid.interfaces.IContextURL`` adapter is found when ``request.resource_url`` is called. Misc ---- - Change ``set_traverser`` API name to ``add_traverser``. Ref #438. --- CHANGES.txt | 54 ++++- docs/api/config.rst | 2 +- docs/api/interfaces.rst | 4 + docs/api/request.rst | 2 + docs/narr/hooks.rst | 59 +++-- docs/whatsnew-1.3.rst | 56 ++++- pyramid/config/factories.py | 77 +++++- pyramid/interfaces.py | 43 +++- pyramid/tests/test_config/test_factories.py | 86 ++++++- pyramid/tests/test_request.py | 25 +- pyramid/tests/test_url.py | 363 +++++++++++++++++++++++++--- pyramid/traversal.py | 66 ++--- pyramid/url.py | 226 ++++++++++++++++- 13 files changed, 931 insertions(+), 132 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 26d547ae6..22f8320f9 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -23,11 +23,17 @@ Features something like "AttributeError: 'NoneType' object has no attribute 'rfind'". -- Add ``pyramid.config.Configurator.set_traverser`` API method. See the +- Add ``pyramid.config.Configurator.add_traverser`` API method. See the Hooks narrative documentation section entitled "Changing the Traverser" for more information. This is not a new feature, it just provides an API for adding a traverser without needing to use the ZCA API. +- Add ``pyramid.config.Configurator.add_resource_url_adapter`` API method. + See the Hooks narrative documentation section entitled "Changing How + pyramid.request.Request.resource_url Generates a URL" for more information. + This is not a new feature, it just provides an API for adding a resource + url adapter without needing to use the ZCA API. + - The system value ``req`` is now supplied to renderers as an alias for ``request``. This means that you can now, for example, in a template, do ``req.route_url(...)`` instead of ``request.route_url(...)``. This is @@ -35,6 +41,52 @@ Features methods and attributes from within templates. The value ``request`` is still available too, this is just an alternative. +- A new interface was added: ``pyramid.interfaces.IResourceURL``. An adapter + implementing its interface can be used to override resource URL generation + when ``request.resource_url`` is called. This interface replaces the + now-deprecated ``pyramid.interfaces.IContextURL`` interface. + +- The dictionary passed to a resource's ``__resource_url__`` method (see + "Overriding Resource URL Generation" in the "Resources" chapter) now + contains an ``app_url`` key, representing the application URL generated + during ``request.resource_url``. It represents a potentially customized + URL prefix, containing potentially custom scheme, host and port information + passed by the user to ``request.resource_url``. It should be used instead + of ``request.application_url`` where necessary. + +- The ``request.resource_url`` API now accepts these arguments: ``app_url``, + ``scheme``, ``host``, and ``port``. The app_url argument can be used to + replace the URL prefix wholesale during url generation. The ``scheme``, + ``host``, and ``port`` arguments can be used to replace the respective + default values of ``request.application_url`` partially. + +- A new API named ``request.resource_path`` now exists. It works like + ``request.resource_url`` but produces a relative URL rather than an + absolute one. + +- The ``request.route_url`` API now accepts these arguments: ``_app_url``, + ``_scheme``, ``_host``, and ``_port``. The ``_app_url`` argument can be + used to replace the URL prefix wholesale during url generation. The + ``_scheme``, ``_host``, and ``_port`` arguments can be used to replace the + respective default values of ``request.application_url`` partially. + +Backwards Incompatibilities +--------------------------- + +- The ``pyramid.interfaces.IContextURL`` interface has been deprecated. + People have been instructed to use this to register a resource url adapter + in the "Hooks" chapter to use to influence ``request.resource_url`` URL + generation for resources found via custom traversers since Pyramid 1.0. + + The interface still exists and registering such an adapter still works, but + this interface will be removed from the software after a few major Pyramid + releases. You should replace it with an equivalent + ``pyramid.interfaces.IResourceURL`` adapter, registered using the new + ``pyramid.config.Configurator.add_resource_url_adapter`` API. A + deprecation warning is now emitted when a + ``pyramid.interfaces.IContextURL`` adapter is found when + ``request.resource_url`` is called. + Documentation ------------- diff --git a/docs/api/config.rst b/docs/api/config.rst index 3c5ee563a..3fc2cfc44 100644 --- a/docs/api/config.rst +++ b/docs/api/config.rst @@ -94,7 +94,7 @@ .. automethod:: set_notfound_view - .. automethod:: set_traverser + .. automethod:: add_traverser .. automethod:: set_renderer_globals_factory(factory) diff --git a/docs/api/interfaces.rst b/docs/api/interfaces.rst index 11cd8cf7e..1dea5fab0 100644 --- a/docs/api/interfaces.rst +++ b/docs/api/interfaces.rst @@ -79,3 +79,7 @@ Other Interfaces .. autointerface:: IAssetDescriptor :members: + + .. autointerface:: IResourceURL + :members: + diff --git a/docs/api/request.rst b/docs/api/request.rst index 1ab84e230..e1b233fbc 100644 --- a/docs/api/request.rst +++ b/docs/api/request.rst @@ -183,6 +183,8 @@ .. automethod:: resource_url + .. automethod:: resource_path + .. attribute:: response_* In Pyramid 1.0, you could set attributes on a diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 076f9fa5c..2c4310080 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -479,58 +479,55 @@ When you add a traverser as described in :ref:`changing_the_traverser`, it's often convenient to continue to use the :meth:`pyramid.request.Request.resource_url` API. However, since the way traversal is done will have been modified, the URLs it generates by default -may be incorrect. +may be incorrect when used against resources derived from your custom +traverser. If you've added a traverser, you can change how :meth:`~pyramid.request.Request.resource_url` generates a URL for a specific -type of resource by adding a registerAdapter call for -:class:`pyramid.interfaces.IContextURL` to your application: +type of resource by adding a call to +:meth:`pyramid.config.add_resource_url_adapter`. + +For example: .. code-block:: python :linenos: - from pyramid.interfaces import ITraverser - from zope.interface import Interface - from myapp.traversal import URLGenerator + from myapp.traversal import ResourceURLAdapter from myapp.resources import MyRoot - config.registry.registerAdapter(URLGenerator, (MyRoot, Interface), - IContextURL) + config.add_resource_url_adapter(ResourceURLAdapter, resource_iface=MyRoot) -In the above example, the ``myapp.traversal.URLGenerator`` class will be used -to provide services to :meth:`~pyramid.request.Request.resource_url` any time -the :term:`context` passed to ``resource_url`` is of class -``myapp.resources.MyRoot``. The second argument in the ``(MyRoot, -Interface)`` tuple represents the type of interface that must be possessed by -the :term:`request` (in this case, any interface, represented by -``zope.interface.Interface``). +In the above example, the ``myapp.traversal.ResourceURLAdapter`` class will +be used to provide services to :meth:`~pyramid.request.Request.resource_url` +any time the :term:`resource` passed to ``resource_url`` is of the class +``myapp.resources.MyRoot``. The ``resource_iface`` argument ``MyRoot`` +represents the type of interface that must be possessed by the resource for +this resource url factory to be found. If the ``resource_iface`` argument is +omitted, this resource url adapter will be used for *all* resources. -The API that must be implemented by a class that provides -:class:`~pyramid.interfaces.IContextURL` is as follows: +The API that must be implemented by your a class that provides +:class:`~pyramid.interfaces.IResourceURL` is as follows: .. code-block:: python :linenos: - from zope.interface import Interface - - class IContextURL(Interface): - """ An adapter which deals with URLs related to a context. + class MyResourceURL(object): + """ An adapter which provides the virtual and physical paths of a + resource """ - def __init__(self, context, request): - """ Accept the context and request """ - - def virtual_root(self): - """ Return the virtual root object related to a request and the - current context""" - - def __call__(self): - """ Return a URL that points to the context """ + def __init__(self, resource, request): + """ Accept the resource and request and set self.physical_path and + self.virtual_path""" + self.virtual_path = some_function_of(resource, request) + self.physical_path = some_other_function_of(resource, request) The default context URL generator is available for perusal as the class -:class:`pyramid.traversal.TraversalContextURL` in the `traversal module +:class:`pyramid.traversal.ResourceURL` in the `traversal module `_ of the :term:`Pylons` GitHub Pyramid repository. +See :meth:`pyramid.config.add_resource_url_adapter` for more information. + .. index:: single: IResponse single: special view responses diff --git a/docs/whatsnew-1.3.rst b/docs/whatsnew-1.3.rst index a27ef6af9..7d1c9217d 100644 --- a/docs/whatsnew-1.3.rst +++ b/docs/whatsnew-1.3.rst @@ -260,16 +260,21 @@ Minor Feature Additions http://readthedocs.org/docs/venusian/en/latest/#ignore-scan-argument for more information about how to use the ``ignore`` argument to ``scan``. -- Add :meth:`pyramid.config.Configurator.set_traverser` API method. See +- Add :meth:`pyramid.config.Configurator.add_traverser` API method. See :ref:`changing_the_traverser` for more information. This is not a new feature, it just provides an API for adding a traverser without needing to use the ZCA API. +- Add :meth:`pyramid.config.Configurator.add_resource_url_adapter` API + method. See :ref:`changing_resource_url` for more information. This is + not a new feature, it just provides an API for adding a resource url + adapter without needing to use the ZCA API. + - The :meth:`pyramid.config.Configurator.scan` method can now be passed an ``ignore`` argument, which can be a string, a callable, or a list consisting of strings and/or callables. This feature allows submodules, subpackages, and global objects from being scanned. See - http://readthedocs.org/docs/venusian/en/latest/#ignore-scan-argument for + http://readthedocs.org/docs/venusian/en/latest/#ignore-scan-argument for more information about how to use the ``ignore`` argument to ``scan``. - Better error messages when a view callable returns a value that cannot be @@ -291,6 +296,38 @@ Minor Feature Additions methods and attributes from within templates. The value ``request`` is still available too, this is just an alternative. +- A new interface was added: :class:`pyramid.interfaces.IResourceURL`. An + adapter implementing its interface can be used to override resource URL + generation when :meth:`pyramid.request.Request.resource_url` is called. + This interface replaces the now-deprecated + ``pyramid.interfaces.IContextURL`` interface. + +- The dictionary passed to a resource's ``__resource_url__`` method (see + :ref:`overriding_resource_url_generation`) now contains an ``app_url`` key, + representing the application URL generated during + :meth:`pyramid.request.Request.resource_url`. It represents a potentially + customized URL prefix, containing potentially custom scheme, host and port + information passed by the user to ``request.resource_url``. It should be + used instead of ``request.application_url`` where necessary. + +- The :meth:`pyramid.request.Request.resource_url` API now accepts these + arguments: ``app_url``, ``scheme``, ``host``, and ``port``. The app_url + argument can be used to replace the URL prefix wholesale during url + generation. The ``scheme``, ``host``, and ``port`` arguments can be used + to replace the respective default values of ``request.application_url`` + partially. + +- A new API named :meth:`pyramid.request.Request.resource_path` now exists. + It works like :meth:`pyramid.request.Request.resource_url`` but produces a + relative URL rather than an absolute one. + +- The :meth:`pyramid.request.Request.route_url` API now accepts these + arguments: ``_app_url``, ``_scheme``, ``_host``, and ``_port``. The + ``_app_url`` argument can be used to replace the URL prefix wholesale + during url generation. The ``_scheme``, ``_host``, and ``_port`` arguments + can be used to replace the respective default values of + ``request.application_url`` partially. + Backwards Incompatibilities --------------------------- @@ -360,6 +397,21 @@ Backwards Incompatibilities no negative affect because the implementation was broken for dict-based arguments. +- The ``pyramid.interfaces.IContextURL`` interface has been deprecated. + People have been instructed to use this to register a resource url adapter + in the "Hooks" chapter to use to influence + :meth:`pyramid.request.Request.resource_url` URL generation for resources + found via custom traversers since Pyramid 1.0. + + The interface still exists and registering such an adapter still works, but + this interface will be removed from the software after a few major Pyramid + releases. You should replace it with an equivalent + :class:`pyramid.interfaces.IResourceURL` adapter, registered using the new + :meth:`pyramid.config.Configurator.add_resource_url_adapter` API. A + deprecation warning is now emitted when a + ``pyramid.interfaces.IContextURL`` adapter is found when + :meth:`pyramid.request.Request.resource_url` is called. + Documentation Enhancements -------------------------- diff --git a/pyramid/config/factories.py b/pyramid/config/factories.py index 7c0ea054d..76f8d86ed 100644 --- a/pyramid/config/factories.py +++ b/pyramid/config/factories.py @@ -10,6 +10,7 @@ from pyramid.interfaces import ( IRootFactory, ISessionFactory, ITraverser, + IResourceURL, ) from pyramid.traversal import DefaultRootFactory @@ -143,7 +144,8 @@ class FactoriesConfiguratorMixin(object): self.action(('request properties', name), register, introspectables=(intr,)) - def set_traverser(self, factory, iface=None): + @action_method + def add_traverser(self, factory, iface=None): """ The superdefault :term:`traversal` algorithm that :app:`Pyramid` uses is explained in :ref:`traversal_algorithm`. Though it is rarely @@ -158,7 +160,7 @@ class FactoriesConfiguratorMixin(object): .. code-block:: python from myapp.traversal import MyCustomTraverser - config.set_traverser(MyCustomTraverser) + config.add_traverser(MyCustomTraverser) This would cause the Pyramid superdefault traverser to never be used; intead all traversal would be done using your ``MyCustomTraverser`` @@ -186,7 +188,7 @@ class FactoriesConfiguratorMixin(object): .. code-block:: python - config.set_traverser(MyCustomTraverser, MyRootClass) + config.add_traverser(MyCustomTraverser, MyRootClass) When more than one traverser is active, the "most specific" traverser will be used (the one that matches the class or interface of the @@ -212,7 +214,74 @@ class FactoriesConfiguratorMixin(object): ) intr['factory'] = factory intr['iface'] = iface - self.action(('traverser', iface), register, introspectables=(intr,)) + self.action(discriminator, register, introspectables=(intr,)) + + @action_method + def add_resource_url_adapter(self, factory, resource_iface=None, + request_iface=None): + """ + When you add a traverser as described in + :ref:`changing_the_traverser`, it's convenient to continue to use the + :meth:`pyramid.request.Request.resource_url` API. However, since the + way traversal is done may have been modified, the URLs that + ``resource_url`` generates by default may be incorrect when resources + are returned by a custom traverser. + + If you've added a traverser, you can change how + :meth:`~pyramid.request.Request.resource_url` generates a URL for a + specific type of resource by calling this method. + + The ``factory`` argument represents a class that implements the + :class:`~pyramid.interfaces.IResourceURL` interface. The class + constructor should accept two arguments in its constructor (the + resource and the request) and the resulting instance should provide + the attributes detailed in that interface (``virtual_path`` and + ``physical_path``, in particular). + + The ``resource_iface`` argument represents a class or interface that + the resource should possess for this url adapter to be used when + :meth:`pyramid.request.Request.resource_url` looks up a resource url + adapter. If ``resource_iface`` is not passed, or it is passed as + ``None``, the adapter will be used for every type of resource. + + The ``request_iface`` argument represents a class or interface that + the request should possess for this url adapter to be used when + :meth:`pyramid.request.Request.resource_url` looks up a resource url + adapter. If ``request_iface`` is not epassed, or it is passed as + ``None``, the adapter will be used for every type of request. + + See :ref:`changing_resource_url` for more information. + + .. note:: + + This API is new in Pyramid 1.3. + """ + factory = self.maybe_dotted(factory) + resource_iface = self.maybe_dotted(resource_iface) + request_iface = self.maybe_dotted(request_iface) + def register(resource_iface=resource_iface, + request_iface=request_iface): + if resource_iface is None: + resource_iface = Interface + if request_iface is None: + request_iface = Interface + self.registry.registerAdapter( + factory, + (resource_iface, request_iface), + IResourceURL, + ) + discriminator = ('resource url adapter', resource_iface, request_iface) + intr = self.introspectable( + 'resource url adapters', + discriminator, + 'resource url adapter for resource iface %r, request_iface %r' % ( + resource_iface, request_iface), + 'resource url adapter', + ) + intr['factory'] = factory + intr['resource_iface'] = resource_iface + intr['request_iface'] = request_iface + self.action(discriminator, register, introspectables=(intr,)) def _set_request_properties(event): request = event.request diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 8de5331b9..5b9edf31a 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -731,15 +731,54 @@ class IRoutesMapper(Interface): ``match`` key will be the matchdict or ``None`` if no route matched. Static routes will not be considered for matching. """ -class IContextURL(Interface): +class IResourceURL(Interface): + virtual_path = Attribute('The virtual url path of the resource.') + physical_path = Attribute('The physical url path of the resource.') + +class IContextURL(IResourceURL): """ An adapter which deals with URLs related to a context. + + ..warning:: + + This interface is deprecated as of Pyramid 1.3 with the introduction of + IResourceURL. """ + # this class subclasses IResourceURL because request.resource_url looks + # for IResourceURL via queryAdapter. queryAdapter will find a deprecated + # IContextURL registration if no registration for IResourceURL exists. + # In reality, however, IContextURL objects were never required to have + # the virtual_path or physical_path attributes spelled in IResourceURL. + # The inheritance relationship is purely to benefit adapter lookup, + # not to imply an inheritance relationship of interface attributes + # and methods. + # + # Mechanics: + # + # class Fudge(object): + # def __init__(self, one, two): + # print one, two + # class Another(object): + # def __init__(self, one, two): + # print one, two + # ob = object() + # r.registerAdapter(Fudge, (Interface, Interface), IContextURL) + # print r.queryMultiAdapter((ob, ob), IResourceURL) + # r.registerAdapter(Another, (Interface, Interface), IResourceURL) + # print r.queryMultiAdapter((ob, ob), IResourceURL) + # + # prints + # + # + # <__main__.Fudge object at 0x1cda890> + # + # <__main__.Another object at 0x1cda850> + def virtual_root(): """ Return the virtual root related to a request and the current context""" def __call__(): - """ Return a URL that points to the context """ + """ Return a URL that points to the context. """ class IPackageOverrides(Interface): """ Utility for pkg_resources overrides """ diff --git a/pyramid/tests/test_config/test_factories.py b/pyramid/tests/test_config/test_factories.py index 51c60896e..5f300a73e 100644 --- a/pyramid/tests/test_config/test_factories.py +++ b/pyramid/tests/test_config/test_factories.py @@ -129,10 +129,10 @@ class TestFactoriesMixin(unittest.TestCase): self.assertEqual(callables, [('foo', foo, False), ('bar', foo, True)]) - def test_set_traverser_dotted_names(self): + def test_add_traverser_dotted_names(self): from pyramid.interfaces import ITraverser config = self._makeOne(autocommit=True) - config.set_traverser( + config.add_traverser( 'pyramid.tests.test_config.test_factories.DummyTraverser', 'pyramid.tests.test_config.test_factories.DummyIface') iface = DummyIface() @@ -140,25 +140,25 @@ class TestFactoriesMixin(unittest.TestCase): self.assertEqual(traverser.__class__, DummyTraverser) self.assertEqual(traverser.root, iface) - def test_set_traverser_default_iface_means_Interface(self): + def test_add_traverser_default_iface_means_Interface(self): from pyramid.interfaces import ITraverser config = self._makeOne(autocommit=True) - config.set_traverser(DummyTraverser) + config.add_traverser(DummyTraverser) traverser = config.registry.getAdapter(None, ITraverser) self.assertEqual(traverser.__class__, DummyTraverser) - def test_set_traverser_nondefault_iface(self): + def test_add_traverser_nondefault_iface(self): from pyramid.interfaces import ITraverser config = self._makeOne(autocommit=True) - config.set_traverser(DummyTraverser, DummyIface) + config.add_traverser(DummyTraverser, DummyIface) iface = DummyIface() traverser = config.registry.getAdapter(iface, ITraverser) self.assertEqual(traverser.__class__, DummyTraverser) self.assertEqual(traverser.root, iface) - def test_set_traverser_introspectables(self): + def test_add_traverser_introspectables(self): config = self._makeOne() - config.set_traverser(DummyTraverser, DummyIface) + config.add_traverser(DummyTraverser, DummyIface) actions = config.action_state.actions self.assertEqual(len(actions), 1) intrs = actions[0]['introspectables'] @@ -169,6 +169,70 @@ class TestFactoriesMixin(unittest.TestCase): self.assertEqual(intr.category_name, 'traversers') self.assertEqual(intr.title, 'traverser for %r' % DummyIface) + def test_add_resource_url_adapter_dotted_names(self): + from pyramid.interfaces import IResourceURL + config = self._makeOne(autocommit=True) + config.add_resource_url_adapter( + 'pyramid.tests.test_config.test_factories.DummyResourceURL', + 'pyramid.tests.test_config.test_factories.DummyIface', + 'pyramid.tests.test_config.test_factories.DummyIface', + ) + iface = DummyIface() + adapter = config.registry.getMultiAdapter((iface, iface), + IResourceURL) + self.assertEqual(adapter.__class__, DummyResourceURL) + self.assertEqual(adapter.resource, iface) + self.assertEqual(adapter.request, iface) + + def test_add_resource_url_default_interfaces_mean_Interface(self): + from pyramid.interfaces import IResourceURL + config = self._makeOne(autocommit=True) + config.add_resource_url_adapter(DummyResourceURL) + iface = DummyIface() + adapter = config.registry.getMultiAdapter((iface, iface), + IResourceURL) + self.assertEqual(adapter.__class__, DummyResourceURL) + self.assertEqual(adapter.resource, iface) + self.assertEqual(adapter.request, iface) + + def test_add_resource_url_nodefault_interfaces(self): + from zope.interface import Interface + from pyramid.interfaces import IResourceURL + config = self._makeOne(autocommit=True) + config.add_resource_url_adapter(DummyResourceURL, DummyIface, + DummyIface) + iface = DummyIface() + adapter = config.registry.getMultiAdapter((iface, iface), + IResourceURL) + self.assertEqual(adapter.__class__, DummyResourceURL) + self.assertEqual(adapter.resource, iface) + self.assertEqual(adapter.request, iface) + bad_result = config.registry.queryMultiAdapter( + (Interface, Interface), + IResourceURL, + ) + self.assertEqual(bad_result, None) + + def test_add_resource_url_adapter_introspectables(self): + config = self._makeOne() + config.add_resource_url_adapter(DummyResourceURL, DummyIface) + actions = config.action_state.actions + self.assertEqual(len(actions), 1) + intrs = actions[0]['introspectables'] + self.assertEqual(len(intrs), 1) + intr = intrs[0] + self.assertEqual(intr.type_name, 'resource url adapter') + self.assertEqual(intr.discriminator, + ('resource url adapter', DummyIface, None)) + self.assertEqual(intr.category_name, 'resource url adapters') + self.assertEqual( + intr.title, + "resource url adapter for resource iface " + ", " + "request_iface None" + ) + + class DummyRequest(object): callables = None @@ -186,3 +250,9 @@ class DummyTraverser(object): class DummyIface(object): pass + +class DummyResourceURL(object): + def __init__(self, resource, request): + self.resource = resource + self.request = request + diff --git a/pyramid/tests/test_request.py b/pyramid/tests/test_request.py index 10cda96d8..8a5215a2b 100644 --- a/pyramid/tests/test_request.py +++ b/pyramid/tests/test_request.py @@ -21,17 +21,16 @@ class TestRequest(unittest.TestCase): from pyramid.request import Request return Request - def _registerContextURL(self): - from pyramid.interfaces import IContextURL + def _registerResourceURL(self): + from pyramid.interfaces import IResourceURL from zope.interface import Interface - class DummyContextURL(object): + class DummyResourceURL(object): def __init__(self, context, request): - pass - def __call__(self): - return 'http://example.com/context/' + self.physical_path = '/context/' + self.virtual_path = '/context/' self.config.registry.registerAdapter( - DummyContextURL, (Interface, Interface), - IContextURL) + DummyResourceURL, (Interface, Interface), + IResourceURL) def test_charset_defaults_to_utf8(self): r = self._makeOne({'PATH_INFO':'/'}) @@ -151,8 +150,14 @@ class TestRequest(unittest.TestCase): self.assertEqual(inst.finished_callbacks, []) def test_resource_url(self): - self._registerContextURL() - inst = self._makeOne({}) + self._registerResourceURL() + environ = { + 'PATH_INFO':'/', + 'SERVER_NAME':'example.com', + 'SERVER_PORT':'80', + 'wsgi.url_scheme':'http', + } + inst = self._makeOne(environ) root = DummyContext() result = inst.resource_url(root) self.assertEqual(result, 'http://example.com/context/') diff --git a/pyramid/tests/test_url.py b/pyramid/tests/test_url.py index 4c39d8e9c..3c36363ed 100644 --- a/pyramid/tests/test_url.py +++ b/pyramid/tests/test_url.py @@ -1,4 +1,5 @@ import unittest +import warnings from pyramid.testing import setUp from pyramid.testing import tearDown @@ -12,11 +13,16 @@ class TestURLMethodsMixin(unittest.TestCase): def tearDown(self): tearDown() - def _makeOne(self): + def _makeOne(self, environ=None): from pyramid.url import URLMethodsMixin + if environ is None: + environ = {} class Request(URLMethodsMixin): application_url = 'http://example.com:5432' - request = Request() + script_name = '' + def __init__(self, environ): + self.environ = environ + request = Request(environ) request.registry = self.config.registry return request @@ -31,114 +37,124 @@ class TestURLMethodsMixin(unittest.TestCase): reg.registerAdapter(DummyContextURL, (Interface, Interface), IContextURL) + def _registerResourceURL(self, reg): + from pyramid.interfaces import IResourceURL + from zope.interface import Interface + class DummyResourceURL(object): + def __init__(self, context, request): + self.physical_path = '/context/' + self.virtual_path = '/context/' + reg.registerAdapter(DummyResourceURL, (Interface, Interface), + IResourceURL) + def test_resource_url_root_default(self): request = self._makeOne() - self._registerContextURL(request.registry) + self._registerResourceURL(request.registry) root = DummyContext() result = request.resource_url(root) - self.assertEqual(result, 'http://example.com/context/') + self.assertEqual(result, 'http://example.com:5432/context/') def test_resource_url_extra_args(self): request = self._makeOne() - self._registerContextURL(request.registry) + self._registerResourceURL(request.registry) context = DummyContext() result = request.resource_url(context, 'this/theotherthing', 'that') self.assertEqual( result, - 'http://example.com/context/this%2Ftheotherthing/that') + 'http://example.com:5432/context/this%2Ftheotherthing/that') def test_resource_url_unicode_in_element_names(self): request = self._makeOne() - self._registerContextURL(request.registry) + self._registerResourceURL(request.registry) uc = text_(b'La Pe\xc3\xb1a', 'utf-8') context = DummyContext() result = request.resource_url(context, uc) self.assertEqual(result, - 'http://example.com/context/La%20Pe%C3%B1a') + 'http://example.com:5432/context/La%20Pe%C3%B1a') def test_resource_url_at_sign_in_element_names(self): request = self._makeOne() - self._registerContextURL(request.registry) + self._registerResourceURL(request.registry) context = DummyContext() result = request.resource_url(context, '@@myview') self.assertEqual(result, - 'http://example.com/context/@@myview') + 'http://example.com:5432/context/@@myview') def test_resource_url_element_names_url_quoted(self): request = self._makeOne() - self._registerContextURL(request.registry) + self._registerResourceURL(request.registry) context = DummyContext() result = request.resource_url(context, 'a b c') - self.assertEqual(result, 'http://example.com/context/a%20b%20c') + self.assertEqual(result, 'http://example.com:5432/context/a%20b%20c') def test_resource_url_with_query_dict(self): request = self._makeOne() - self._registerContextURL(request.registry) + self._registerResourceURL(request.registry) context = DummyContext() uc = text_(b'La Pe\xc3\xb1a', 'utf-8') result = request.resource_url(context, 'a', query={'a':uc}) self.assertEqual(result, - 'http://example.com/context/a?a=La+Pe%C3%B1a') + 'http://example.com:5432/context/a?a=La+Pe%C3%B1a') def test_resource_url_with_query_seq(self): request = self._makeOne() - self._registerContextURL(request.registry) + self._registerResourceURL(request.registry) context = DummyContext() uc = text_(b'La Pe\xc3\xb1a', 'utf-8') result = request.resource_url(context, 'a', query=[('a', 'hi there'), ('b', uc)]) self.assertEqual(result, - 'http://example.com/context/a?a=hi+there&b=La+Pe%C3%B1a') + 'http://example.com:5432/context/a?a=hi+there&b=La+Pe%C3%B1a') def test_resource_url_anchor_is_after_root_when_no_elements(self): request = self._makeOne() - self._registerContextURL(request.registry) + self._registerResourceURL(request.registry) context = DummyContext() result = request.resource_url(context, anchor='a') self.assertEqual(result, - 'http://example.com/context/#a') + 'http://example.com:5432/context/#a') def test_resource_url_anchor_is_after_elements_when_no_qs(self): request = self._makeOne() - self._registerContextURL(request.registry) + self._registerResourceURL(request.registry) context = DummyContext() result = request.resource_url(context, 'a', anchor='b') self.assertEqual(result, - 'http://example.com/context/a#b') + 'http://example.com:5432/context/a#b') def test_resource_url_anchor_is_after_qs_when_qs_is_present(self): request = self._makeOne() - self._registerContextURL(request.registry) + self._registerResourceURL(request.registry) context = DummyContext() result = request.resource_url(context, 'a', query={'b':'c'}, anchor='d') self.assertEqual(result, - 'http://example.com/context/a?b=c#d') + 'http://example.com:5432/context/a?b=c#d') def test_resource_url_anchor_is_encoded_utf8_if_unicode(self): request = self._makeOne() - self._registerContextURL(request.registry) + self._registerResourceURL(request.registry) context = DummyContext() uc = text_(b'La Pe\xc3\xb1a', 'utf-8') result = request.resource_url(context, anchor=uc) self.assertEqual( result, native_( - text_(b'http://example.com/context/#La Pe\xc3\xb1a', + text_(b'http://example.com:5432/context/#La Pe\xc3\xb1a', 'utf-8'), 'utf-8') ) def test_resource_url_anchor_is_not_urlencoded(self): request = self._makeOne() - self._registerContextURL(request.registry) + self._registerResourceURL(request.registry) context = DummyContext() result = request.resource_url(context, anchor=' /#') self.assertEqual(result, - 'http://example.com/context/# /#') + 'http://example.com:5432/context/# /#') - def test_resource_url_no_IContextURL_registered(self): - # falls back to TraversalContextURL + def test_resource_url_no_IResourceURL_registered(self): + # falls back to ResourceURL root = DummyContext() root.__name__ = '' root.__parent__ = None @@ -149,12 +165,98 @@ class TestURLMethodsMixin(unittest.TestCase): def test_resource_url_no_registry_on_request(self): request = self._makeOne() - self._registerContextURL(request.registry) + self._registerResourceURL(request.registry) del request.registry root = DummyContext() result = request.resource_url(root) + self.assertEqual(result, 'http://example.com:5432/context/') + + def test_resource_url_finds_IContextURL(self): + request = self._makeOne() + self._registerContextURL(request.registry) + root = DummyContext() + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter('always') + result = request.resource_url(root) + self.assertEqual(len(w), 1) self.assertEqual(result, 'http://example.com/context/') + + def test_resource_url_with_app_url(self): + request = self._makeOne() + self._registerResourceURL(request.registry) + root = DummyContext() + result = request.resource_url(root, app_url='http://somewhere.com') + self.assertEqual(result, 'http://somewhere.com/context/') + + def test_resource_url_with_scheme(self): + environ = { + 'wsgi.url_scheme':'http', + 'SERVER_PORT':'8080', + 'SERVER_NAME':'example.com', + } + request = self._makeOne(environ) + self._registerResourceURL(request.registry) + root = DummyContext() + result = request.resource_url(root, scheme='https') + self.assertEqual(result, 'https://example.com/context/') + + def test_resource_url_with_host(self): + environ = { + 'wsgi.url_scheme':'http', + 'SERVER_PORT':'8080', + 'SERVER_NAME':'example.com', + } + request = self._makeOne(environ) + self._registerResourceURL(request.registry) + root = DummyContext() + result = request.resource_url(root, host='someotherhost.com') + self.assertEqual(result, 'http://someotherhost.com:8080/context/') + + def test_resource_url_with_port(self): + environ = { + 'wsgi.url_scheme':'http', + 'SERVER_PORT':'8080', + 'SERVER_NAME':'example.com', + } + request = self._makeOne(environ) + self._registerResourceURL(request.registry) + root = DummyContext() + result = request.resource_url(root, port='8181') + self.assertEqual(result, 'http://example.com:8181/context/') + + def test_resource_url_with_local_url(self): + environ = { + 'wsgi.url_scheme':'http', + 'SERVER_PORT':'8080', + 'SERVER_NAME':'example.com', + } + request = self._makeOne(environ) + self._registerResourceURL(request.registry) + root = DummyContext() + def resource_url(req, info): + self.assertEqual(req, request) + self.assertEqual(info['virtual_path'], '/context/') + self.assertEqual(info['physical_path'], '/context/') + self.assertEqual(info['app_url'], 'http://example.com:5432') + return 'http://example.com/contextabc/' + root.__resource_url__ = resource_url + result = request.resource_url(root) + self.assertEqual(result, 'http://example.com/contextabc/') + + def test_resource_path(self): + request = self._makeOne() + self._registerResourceURL(request.registry) + root = DummyContext() + result = request.resource_path(root) + self.assertEqual(result, '/context/') + def test_resource_path_kwarg(self): + request = self._makeOne() + self._registerResourceURL(request.registry) + root = DummyContext() + result = request.resource_path(root, anchor='abc') + self.assertEqual(result, '/context/#abc') + def test_route_url_with_elements(self): from pyramid.interfaces import IRoutesMapper request = self._makeOne() @@ -234,6 +336,47 @@ class TestURLMethodsMixin(unittest.TestCase): self.assertEqual(result, 'http://example2.com/1/2/3') + def test_route_url_with_host(self): + from pyramid.interfaces import IRoutesMapper + environ = { + 'wsgi.url_scheme':'http', + 'SERVER_PORT':'5432', + } + request = self._makeOne(environ) + mapper = DummyRoutesMapper(route=DummyRoute('/1/2/3')) + request.registry.registerUtility(mapper, IRoutesMapper) + result = request.route_url('flub', _host='someotherhost.com') + self.assertEqual(result, + 'http://someotherhost.com:5432/1/2/3') + + def test_route_url_with_port(self): + from pyramid.interfaces import IRoutesMapper + environ = { + 'wsgi.url_scheme':'http', + 'SERVER_PORT':'5432', + 'SERVER_NAME':'example.com', + } + request = self._makeOne(environ) + mapper = DummyRoutesMapper(route=DummyRoute('/1/2/3')) + request.registry.registerUtility(mapper, IRoutesMapper) + result = request.route_url('flub', _port='8080') + self.assertEqual(result, + 'http://example.com:8080/1/2/3') + + def test_route_url_with_scheme(self): + from pyramid.interfaces import IRoutesMapper + environ = { + 'wsgi.url_scheme':'http', + 'SERVER_PORT':'5432', + 'SERVER_NAME':'example.com', + } + request = self._makeOne(environ) + mapper = DummyRoutesMapper(route=DummyRoute('/1/2/3')) + request.registry.registerUtility(mapper, IRoutesMapper) + result = request.route_url('flub', _scheme='https') + self.assertEqual(result, + 'https://example.com/1/2/3') + def test_route_url_generation_error(self): from pyramid.interfaces import IRoutesMapper request = self._makeOne() @@ -471,6 +614,168 @@ class TestURLMethodsMixin(unittest.TestCase): {'_app_url':'/foo'}) ) + def test_partial_application_url_with_http_host_default_port_http(self): + environ = { + 'wsgi.url_scheme':'http', + 'HTTP_HOST':'example.com:80', + } + request = self._makeOne(environ) + result = request.partial_application_url() + self.assertEqual(result, 'http://example.com') + + def test_partial_application_url_with_http_host_default_port_https(self): + environ = { + 'wsgi.url_scheme':'https', + 'HTTP_HOST':'example.com:443', + } + request = self._makeOne(environ) + result = request.partial_application_url() + self.assertEqual(result, 'https://example.com') + + def test_partial_application_url_with_http_host_nondefault_port_http(self): + environ = { + 'wsgi.url_scheme':'http', + 'HTTP_HOST':'example.com:8080', + } + request = self._makeOne(environ) + result = request.partial_application_url() + self.assertEqual(result, 'http://example.com:8080') + + def test_partial_application_url_with_http_host_nondefault_port_https(self): + environ = { + 'wsgi.url_scheme':'https', + 'HTTP_HOST':'example.com:4443', + } + request = self._makeOne(environ) + result = request.partial_application_url() + self.assertEqual(result, 'https://example.com:4443') + + def test_partial_application_url_with_http_host_no_colon(self): + environ = { + 'wsgi.url_scheme':'http', + 'HTTP_HOST':'example.com', + 'SERVER_PORT':'80', + } + request = self._makeOne(environ) + result = request.partial_application_url() + self.assertEqual(result, 'http://example.com') + + def test_partial_application_url_no_http_host(self): + environ = { + 'wsgi.url_scheme':'http', + 'SERVER_NAME':'example.com', + 'SERVER_PORT':'80', + } + request = self._makeOne(environ) + result = request.partial_application_url() + self.assertEqual(result, 'http://example.com') + + def test_partial_application_replace_port(self): + environ = { + 'wsgi.url_scheme':'http', + 'SERVER_NAME':'example.com', + 'SERVER_PORT':'80', + } + request = self._makeOne(environ) + result = request.partial_application_url(port=8080) + self.assertEqual(result, 'http://example.com:8080') + + def test_partial_application_replace_scheme_https_special_case(self): + environ = { + 'wsgi.url_scheme':'http', + 'SERVER_NAME':'example.com', + 'SERVER_PORT':'80', + } + request = self._makeOne(environ) + result = request.partial_application_url(scheme='https') + self.assertEqual(result, 'https://example.com') + + def test_partial_application_replace_scheme_https_special_case_avoid(self): + environ = { + 'wsgi.url_scheme':'http', + 'SERVER_NAME':'example.com', + 'SERVER_PORT':'80', + } + request = self._makeOne(environ) + result = request.partial_application_url(scheme='https', port='8080') + self.assertEqual(result, 'https://example.com:8080') + + def test_partial_application_replace_scheme_http_special_case(self): + environ = { + 'wsgi.url_scheme':'https', + 'SERVER_NAME':'example.com', + 'SERVER_PORT':'8080', + } + request = self._makeOne(environ) + result = request.partial_application_url(scheme='http') + self.assertEqual(result, 'http://example.com') + + def test_partial_application_replace_scheme_http_special_case_avoid(self): + environ = { + 'wsgi.url_scheme':'https', + 'SERVER_NAME':'example.com', + 'SERVER_PORT':'8000', + } + request = self._makeOne(environ) + result = request.partial_application_url(scheme='http', port='8080') + self.assertEqual(result, 'http://example.com:8080') + + def test_partial_application_replace_host_no_port(self): + environ = { + 'wsgi.url_scheme':'http', + 'SERVER_NAME':'example.com', + 'SERVER_PORT':'80', + } + request = self._makeOne(environ) + result = request.partial_application_url(host='someotherhost.com') + self.assertEqual(result, 'http://someotherhost.com') + + def test_partial_application_replace_host_with_port(self): + environ = { + 'wsgi.url_scheme':'http', + 'SERVER_NAME':'example.com', + 'SERVER_PORT':'8000', + } + request = self._makeOne(environ) + result = request.partial_application_url(host='someotherhost.com:8080') + self.assertEqual(result, 'http://someotherhost.com:8080') + + def test_partial_application_replace_host_and_port(self): + environ = { + 'wsgi.url_scheme':'http', + 'SERVER_NAME':'example.com', + 'SERVER_PORT':'80', + } + request = self._makeOne(environ) + result = request.partial_application_url(host='someotherhost.com:8080', + port='8000') + self.assertEqual(result, 'http://someotherhost.com:8000') + + def test_partial_application_replace_host_port_and_scheme(self): + environ = { + 'wsgi.url_scheme':'http', + 'SERVER_NAME':'example.com', + 'SERVER_PORT':'80', + } + request = self._makeOne(environ) + result = request.partial_application_url( + host='someotherhost.com:8080', + port='8000', + scheme='https', + ) + self.assertEqual(result, 'https://someotherhost.com:8000') + + def test_partial_application_url_with_custom_script_name(self): + environ = { + 'wsgi.url_scheme':'http', + 'SERVER_NAME':'example.com', + 'SERVER_PORT':'8000', + } + request = self._makeOne(environ) + request.script_name = '/abc' + result = request.partial_application_url() + self.assertEqual(result, 'http://example.com:8000/abc') + class Test_route_url(unittest.TestCase): def _callFUT(self, route_name, request, *elements, **kw): from pyramid.url import route_url diff --git a/pyramid/traversal.py b/pyramid/traversal.py index 84dcd33ec..9801f8f18 100644 --- a/pyramid/traversal.py +++ b/pyramid/traversal.py @@ -6,6 +6,7 @@ from zope.interface.interfaces import IInterface from repoze.lru import lru_cache from pyramid.interfaces import ( + IResourceURL, IContextURL, IRequestFactory, ITraverser, @@ -730,17 +731,33 @@ class ResourceTreeTraverser(object): ModelGraphTraverser = ResourceTreeTraverser # b/w compat, not API, used in wild -@implementer(IContextURL) -class TraversalContextURL(object): - """ The IContextURL adapter used to generate URLs for a resource in a - resource tree""" - +@implementer(IResourceURL, IContextURL) +class ResourceURL(object): vroot_varname = VH_ROOT_KEY - def __init__(self, context, request): - self.context = context + def __init__(self, resource, request): + physical_path = resource_path(resource) + if physical_path != '/': + physical_path = physical_path + '/' + + virtual_path = physical_path + + environ = request.environ + vroot_path = environ.get(self.vroot_varname) + + # if the physical path starts with the virtual root path, trim it out + # of the virtual path + if vroot_path is not None: + if physical_path.startswith(vroot_path): + virtual_path = physical_path[len(vroot_path):] + + self.virtual_path = virtual_path + self.physical_path = physical_path + self.resource = resource + self.context = resource # bw compat alias for IContextURL compat self.request = request + # IContextURL method (deprecated in 1.3) def virtual_root(self): environ = self.request.environ vroot_varname = self.vroot_varname @@ -753,6 +770,7 @@ class TraversalContextURL(object): except AttributeError: return find_root(self.context) + # IContextURL method (deprecated in 1.3) def __call__(self): """ Generate a URL based on the :term:`lineage` of a :term:`resource` object that is ``self.context``. If any resource in the context @@ -762,35 +780,21 @@ class TraversalContextURL(object): 'virtual root path': the path of the URL generated by this will be left-stripped of this virtual root path value. """ - resource = self.context - physical_path = resource_path(resource) - if physical_path != '/': - physical_path = physical_path + '/' - virtual_path = physical_path - - request = self.request - environ = request.environ - vroot_varname = self.vroot_varname - vroot_path = environ.get(vroot_varname) - - # if the physical path starts with the virtual root path, trim it out - # of the virtual path - if vroot_path is not None: - if physical_path.startswith(vroot_path): - virtual_path = physical_path[len(vroot_path):] - - local_url = getattr(resource, '__resource_url__', None) + local_url = getattr(self.context, '__resource_url__', None) if local_url is not None: - result = local_url(request, - {'virtual_path':virtual_path, - 'physical_path':physical_path}, - ) + result = local_url( + self.request, + {'virtual_path':self.virtual_path, + 'physical_path':self.physical_path}, + ) if result is not None: # allow it to punt by returning ``None`` return result - app_url = request.application_url # never ends in a slash - return app_url + virtual_path + app_url = self.request.application_url # never ends in a slash + return app_url + self.virtual_path + +TraversalContextURL = ResourceURL # bw compat as of 1.3 @lru_cache(1000) def _join_path_tuple(tuple): diff --git a/pyramid/url.py b/pyramid/url.py index e6a508c17..d1c1b6f42 100644 --- a/pyramid/url.py +++ b/pyramid/url.py @@ -1,32 +1,87 @@ """ Utility functions for dealing with URLs in pyramid """ import os +import warnings from repoze.lru import lru_cache from pyramid.interfaces import ( - IContextURL, + IResourceURL, IRoutesMapper, IStaticURLInfo, ) from pyramid.compat import ( native_, + bytes_, text_type, + url_quote, ) from pyramid.encode import urlencode from pyramid.path import caller_package from pyramid.threadlocal import get_current_registry from pyramid.traversal import ( - TraversalContextURL, + ResourceURL, quote_path_segment, ) +PATH_SAFE = '/:@&+$,' # from webob + class URLMethodsMixin(object): """ Request methods mixin for BaseRequest having to do with URL generation """ + def partial_application_url(self, scheme=None, host=None, port=None): + """ + Construct the URL defined by request.application_url, replacing any + of the default scheme, host, or port portions with user-supplied + variants. + + If ``scheme`` is passed as ``https``, and the ``port`` is *not* + passed, the ``port`` value is assumed to ``443``. Likewise, if + ``scheme`` is passed as ``http`` and ``port`` is not passed, the + ``port`` value is assumed to be ``80``. + """ + e = self.environ + if scheme is None: + scheme = e['wsgi.url_scheme'] + else: + if scheme == 'https': + if port is None: + port = '443' + if scheme == 'http': + if port is None: + port = '80' + url = scheme + '://' + if port is not None: + port = str(port) + if host is None: + host = e.get('HTTP_HOST') + if host is None: + host = e['SERVER_NAME'] + if port is None: + if ':' in host: + host, port = host.split(':', 1) + else: + port = e['SERVER_PORT'] + else: + if ':' in host: + host, _ = host.split(':', 1) + if scheme == 'https': + if port == '443': + port = None + elif scheme == 'http': + if port == '80': + port = None + url += host + if port: + url += ':%s' % port + + url_encoding = getattr(self, 'url_encoding', 'utf-8') # webob 1.2b3+ + bscript_name = bytes_(self.script_name, url_encoding) + return url + url_quote(bscript_name, PATH_SAFE) + def route_url(self, route_name, *elements, **kw): """Generates a fully qualified URL for a named :app:`Pyramid` :term:`route configuration`. @@ -105,6 +160,15 @@ class URLMethodsMixin(object): element will always follow the query element, e.g. ``http://example.com?foo=1#bar``. + If any of ``_scheme``, ``_host``, or ``_port`` is passed and is + non-``None``, the provided value will replace the named portion in + the generated URL. If ``_scheme`` is passed as ``https``, and + ``_port`` is not passed, the ``_port`` value is assumed to have been + passed as ``443``. Likewise, if ``_scheme`` is passed as ``http`` + and ``_port`` is not passed, the ``_port`` value is assumed to have + been passed as ``80``. To avoid this behavior, always explicitly pass + ``_port`` whenever you pass ``_scheme``. + If a keyword ``_app_url`` is present, it will be used as the protocol/hostname/port/leading path prefix of the generated URL. For example, using an ``_app_url`` of @@ -116,6 +180,10 @@ class URLMethodsMixin(object): ``request.application_url`` will be used as the prefix (the default). + If both ``_app_url`` and any of ``_scheme``, ``_host``, or ``_port`` + are passed, ``_app_url`` takes precedence and any values passed for + ``_scheme``, ``_host``, and ``_port`` will be ignored. + This function raises a :exc:`KeyError` if the URL cannot be generated due to missing replacement names. Extra replacement names are ignored. @@ -140,6 +208,9 @@ class URLMethodsMixin(object): anchor = '' qs = '' app_url = None + host = None + scheme = None + port = None if '_query' in kw: qs = '?' + urlencode(kw.pop('_query'), doseq=True) @@ -152,6 +223,21 @@ class URLMethodsMixin(object): if '_app_url' in kw: app_url = kw.pop('_app_url') + if '_host' in kw: + host = kw.pop('_host') + + if '_scheme' in kw: + scheme = kw.pop('_scheme') + + if '_port' in kw: + port = kw.pop('_port') + + if app_url is None: + if (scheme is not None or host is not None or port is not None): + app_url = self.partial_application_url(scheme, host, port) + else: + app_url = self.application_url + path = route.generate(kw) # raises KeyError if generate fails if elements: @@ -161,12 +247,6 @@ class URLMethodsMixin(object): else: suffix = '' - if app_url is None: - # we only defer lookup of application_url until here because - # it's somewhat expensive; we won't need to do it if we've - # been passed _app_url - app_url = self.application_url - return app_url + path + suffix + qs + anchor def route_path(self, route_name, *elements, **kw): @@ -206,7 +286,7 @@ class URLMethodsMixin(object): :term:`resource` object based on the ``wsgi.url_scheme``, ``HTTP_HOST`` or ``SERVER_NAME`` in the request, plus any ``SCRIPT_NAME``. The overall result of this method is always a - UTF-8 encoded string (never Unicode). + UTF-8 encoded string. Examples:: @@ -226,6 +306,10 @@ class URLMethodsMixin(object): http://example.com/a.html#abc + request.resource_url(resource, app_url='') => + + / + Any positional arguments passed in as ``elements`` must be strings Unicode objects, or integer objects. These will be joined by slashes and appended to the generated resource URL. Each of the elements @@ -275,6 +359,38 @@ class URLMethodsMixin(object): will always follow the query element, e.g. ``http://example.com?foo=1#bar``. + If any of the keyword arguments ``scheme``, ``host``, or ``port`` is + passed and is non-``None``, the provided value will replace the named + portion in the generated URL. For example, if you pass + ``scheme='https'``, and the URL that would be generated without the + scheme replacement is ``http://foo.com``, the result will be + ``https://foo.com``. + + If ``scheme`` is passed as ``https``, and an explicit ``port`` is not + passed, the ``port`` value is assumed to have been passed as ``443``. + Likewise, if ``scheme`` is passed as ``http`` and ``port`` is not + passed, the ``port`` value is assumed to have been passed as + ``80``. To avoid this behavior, always explicitly pass ``port`` + whenever you pass ``scheme``. + + If a keyword argument ``app_url`` is passed and is not ``None``, it + should be a string that will be used as the port/hostname/initial + path portion of the generated URL instead of the default request + application URL. For example, if ``app_url='http://foo'``, then the + resulting url of a resource that has a path of ``/baz/bar`` will be + ``http://foo/baz/bar``. If you want to generate completely relative + URLs with no leading scheme, host, port, or initial path, you can + pass ``app_url=''`. Passing ``app_url=''` when the resource path is + ``/baz/bar`` will return ``/baz/bar``. + + .. note:: + + ``app_url`` is new as of Pyramid 1.3. + + If ``app_url`` is passed and any of ``scheme``, ``port``, or ``host`` + are also passed, ``app_url`` will take precedence and the values + passed for ``scheme``, ``host``, and/or ``port`` will be ignored. + If the ``resource`` passed in has a ``__resource_url__`` method, it will be used to generate the URL (scheme, host, port, path) that for the base resource which is operated upon by this function. See also @@ -305,10 +421,69 @@ class URLMethodsMixin(object): except AttributeError: reg = get_current_registry() # b/c - context_url = reg.queryMultiAdapter((resource, self), IContextURL) - if context_url is None: - context_url = TraversalContextURL(resource, self) - resource_url = context_url() + url_adapter = reg.queryMultiAdapter((resource, self), IResourceURL) + if url_adapter is None: + url_adapter = ResourceURL(resource, self) + + virtual_path = getattr(url_adapter, 'virtual_path', None) + + if virtual_path is None: + # old-style IContextURL adapter (Pyramid 1.2 and previous) + warnings.warn( + 'Pyramid is using an IContextURL adapter to generate a ' + 'resource URL; any "app_url", "host", "port", or "scheme" ' + 'arguments passed to resource_url are being ignored. To ' + 'avoid this behavior, as of Pyramid 1.3, register an ' + 'IResourceURL adapter instead of an IContextURL ' + 'adapter for the resource type(s). IContextURL adapters ' + 'will be ignored in a later major release of Pyramid.', + DeprecationWarning, + 2) + + resource_url = url_adapter() + + else: + # newer-style IResourceURL adapter (Pyramid 1.3 and after) + app_url = None + scheme = None + host = None + port = None + + if 'app_url' in kw: + app_url = kw['app_url'] + + if 'scheme' in kw: + scheme = kw['scheme'] + + if 'host' in kw: + host = kw['host'] + + if 'port' in kw: + port = kw['port'] + + if app_url is None: + if scheme or host or port: + app_url = self.partial_application_url(scheme, host, port) + else: + app_url = self.application_url + + resource_url = None + local_url = getattr(resource, '__resource_url__', None) + + if local_url is not None: + # the resource handles its own url generation + d = dict( + virtual_path = virtual_path, + physical_path = url_adapter.physical_path, + app_url = app_url, + ) + # allow __resource_url__ to punt by returning None + resource_url = local_url(self, d) + + if resource_url is None: + # the resource did not handle its own url generation or the + # __resource_url__ function returned None + resource_url = app_url + virtual_path qs = '' anchor = '' @@ -331,6 +506,31 @@ class URLMethodsMixin(object): model_url = resource_url # b/w compat forever + def resource_path(self, resource, *elements, **kw): + """ + Generates a path (aka a 'relative URL', a URL minus the host, scheme, + and port) for a :term:`resource`. + + This function accepts the same argument as + :meth:`pyramid.request.Request.resource_url` and performs the same + duty. It just omits the host, port, and scheme information in the + return value; only the script_name, path, query parameters, and + anchor data are present in the returned string. + + .. note:: + + Calling ``request.resource_path(resource)`` is the same as calling + ``request.resource_path(resource, app_url=request.script_name)``. + :meth:`pyramid.request.Request.resource_path` is, in fact, + implemented in terms of + :meth:`pyramid.request.Request.resource_url` in just this way. As + a result, any ``app_url`` passed within the ``**kw`` values to + ``route_path`` will be ignored. ``scheme``, ``host``, and + ``port`` are also ignored. + """ + kw['app_url'] = self.script_name + return self.resource_url(resource, *elements, **kw) + def static_url(self, path, **kw): """ Generates a fully qualified URL for a static :term:`asset`. -- cgit v1.2.3 From 308ff91bd70b4dc235eaca49ea4025acefcfe0a1 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 17 Feb 2012 01:12:14 -0500 Subject: garden --- TODO.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/TODO.txt b/TODO.txt index 8ea5e3591..ab26a87a8 100644 --- a/TODO.txt +++ b/TODO.txt @@ -7,8 +7,6 @@ Nice-to-Have - Fix renderers chapter to better document system values passed to template renderers. -- Add set_resource_url_generator method. - - Put includes in development.ini on separate lines and fix project.rst to tell people to comment out only the debugtoolbar include when they want to disable. -- cgit v1.2.3 From 0a6a26ca36f6aff7d03b438eef57f6b8d17123e9 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 17 Feb 2012 08:55:45 -0500 Subject: docs fixes --- docs/narr/resources.rst | 29 +++++++++++++++++++---------- docs/whatsnew-1.3.rst | 2 +- pyramid/url.py | 23 ++++++++++++++--------- 3 files changed, 34 insertions(+), 20 deletions(-) diff --git a/docs/narr/resources.rst b/docs/narr/resources.rst index 83734be9f..a24c44f29 100644 --- a/docs/narr/resources.rst +++ b/docs/narr/resources.rst @@ -303,13 +303,22 @@ The ``__resource_url__`` hook is passed two arguments: ``request`` and two keys: ``physical_path`` - The "physical path" computed for the resource, as defined by - ``pyramid.traversal.resource_path(resource)``. + A string representing the "physical path" computed for the resource, as + defined by ``pyramid.traversal.resource_path(resource)``. It will begin + and end with a slash. ``virtual_path`` - The "virtual path" computed for the resource, as defined by - :ref:`virtual_root_support`. This will be identical to the physical path - if virtual rooting is not enabled. + A string representing the "virtual path" computed for the resource, as + defined by :ref:`virtual_root_support`. This will be identical to the + physical path if virtual rooting is not enabled. It will begin and end + with a slash. + +``app_url`` + A string representing the application URL generated during + ``request.resource_url``. It will not end with a slash. It represents a + potentially customized URL prefix, containing potentially custom scheme, + host and port information passed by the user to ``request.resource_url``. + It should be preferred over use of ``request.application_url``. The ``__resource_url__`` method of a resource should return a string representing a URL. If it cannot override the default, it should return @@ -322,16 +331,16 @@ Here's an example ``__resource_url__`` method. class Resource(object): def __resource_url__(self, request, info): - return request.application_url + info['virtual_path'] + return info['app_url'] + info['virtual_path'] The above example actually just generates and returns the default URL, which -would have been what was returned anyway, but your code can perform arbitrary -logic as necessary. For example, your code may wish to override the hostname -or port number of the generated URL. +would have been what was generated by the default ``resource_url`` machinery, +but your code can perform arbitrary logic as necessary. For example, your +code may wish to override the hostname or port number of the generated URL. Note that the URL generated by ``__resource_url__`` should be fully qualified, should end in a slash, and should not contain any query string or -anchor elements (only path elements) to work best with +anchor elements (only path elements) to work with :meth:`~pyramid.request.Request.resource_url`. .. index:: diff --git a/docs/whatsnew-1.3.rst b/docs/whatsnew-1.3.rst index 7d1c9217d..674b16e11 100644 --- a/docs/whatsnew-1.3.rst +++ b/docs/whatsnew-1.3.rst @@ -274,7 +274,7 @@ Minor Feature Additions ``ignore`` argument, which can be a string, a callable, or a list consisting of strings and/or callables. This feature allows submodules, subpackages, and global objects from being scanned. See - http://readthedocs.org/docs/venusian/en/latest/#ignore-scan-argument for + http://readthedocs.org/docs/venusian/en/latest/#ignore-scan-argument for more information about how to use the ``ignore`` argument to ``scan``. - Better error messages when a view callable returns a value that cannot be diff --git a/pyramid/url.py b/pyramid/url.py index d1c1b6f42..efcf241b7 100644 --- a/pyramid/url.py +++ b/pyramid/url.py @@ -160,13 +160,18 @@ class URLMethodsMixin(object): element will always follow the query element, e.g. ``http://example.com?foo=1#bar``. - If any of ``_scheme``, ``_host``, or ``_port`` is passed and is - non-``None``, the provided value will replace the named portion in - the generated URL. If ``_scheme`` is passed as ``https``, and + If any of the keyword arguments ``_scheme``, ``_host``, or ``_port`` + is passed and is non-``None``, the provided value will replace the + named portion in the generated URL. For example, if you pass + ``_host='foo.com'``, and the URL that would have been generated + without the host replacement is ``http://example.com/a``, the result + will be ``https://foo.com/a``. + + Note that if ``_scheme`` is passed as ``https``, and ``_port`` is not + passed, the ``_port`` value is assumed to have been passed as + ``443``. Likewise, if ``_scheme`` is passed as ``http`` and ``_port`` is not passed, the ``_port`` value is assumed to have been - passed as ``443``. Likewise, if ``_scheme`` is passed as ``http`` - and ``_port`` is not passed, the ``_port`` value is assumed to have - been passed as ``80``. To avoid this behavior, always explicitly pass + passed as ``80``. To avoid this behavior, always explicitly pass ``_port`` whenever you pass ``_scheme``. If a keyword ``_app_url`` is present, it will be used as the @@ -362,9 +367,9 @@ class URLMethodsMixin(object): If any of the keyword arguments ``scheme``, ``host``, or ``port`` is passed and is non-``None``, the provided value will replace the named portion in the generated URL. For example, if you pass - ``scheme='https'``, and the URL that would be generated without the - scheme replacement is ``http://foo.com``, the result will be - ``https://foo.com``. + ``host='foo.com'``, and the URL that would have been generated + without the host replacement is ``http://example.com/a``, the result + will be ``https://foo.com/a``. If ``scheme`` is passed as ``https``, and an explicit ``port`` is not passed, the ``port`` value is assumed to have been passed as ``443``. -- cgit v1.2.3 From 305d23f9e9dd095f4fdface116a2155bd86a453c Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 17 Feb 2012 18:29:10 -0500 Subject: docs fixes --- docs/whatsnew-1.3.rst | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/docs/whatsnew-1.3.rst b/docs/whatsnew-1.3.rst index 674b16e11..acb884d49 100644 --- a/docs/whatsnew-1.3.rst +++ b/docs/whatsnew-1.3.rst @@ -364,9 +364,10 @@ Backwards Incompatibilities and upgrade Pyramid itself "in-place"; it may simply break instead (particularly if you use ZCML's ``includeOverrides`` directive). -- String values passed to ``route_url`` or ``route_path`` that are meant to - replace "remainder" matches will now be URL-quoted except for embedded - slashes. For example:: +- String values passed to :meth:`Pyramid.request.Request.route_url` or + :meth:`Pyramid.request.Request.route_path` that are meant to replace + "remainder" matches will now be URL-quoted except for embedded slashes. For + example:: config.add_route('remain', '/foo*remainder') request.route_path('remain', remainder='abc / def') @@ -385,8 +386,8 @@ Backwards Incompatibilities ``route_path`` or ``route_url`` to do this now. - If you pass a bytestring that contains non-ASCII characters to - ``add_route`` as a pattern, it will now fail at startup time. Use Unicode - instead. + :meth:`pyramid.config.Configurator.add_route` as a pattern, it will now + fail at startup time. Use Unicode instead. - The ``path_info`` route and view predicates now match against ``request.upath_info`` (Unicode) rather than ``request.path_info`` @@ -403,10 +404,11 @@ Backwards Incompatibilities :meth:`pyramid.request.Request.resource_url` URL generation for resources found via custom traversers since Pyramid 1.0. - The interface still exists and registering such an adapter still works, but - this interface will be removed from the software after a few major Pyramid - releases. You should replace it with an equivalent - :class:`pyramid.interfaces.IResourceURL` adapter, registered using the new + The interface still exists and registering an adapter using it as + documented in older versions still works, but this interface will be + removed from the software after a few major Pyramid releases. You should + replace it with an equivalent :class:`pyramid.interfaces.IResourceURL` + adapter, registered using the new :meth:`pyramid.config.Configurator.add_resource_url_adapter` API. A deprecation warning is now emitted when a ``pyramid.interfaces.IContextURL`` adapter is found when -- cgit v1.2.3