diff options
| author | Chris McDonough <chrism@plope.com> | 2010-12-20 23:31:12 -0500 |
|---|---|---|
| committer | Chris McDonough <chrism@plope.com> | 2010-12-20 23:31:12 -0500 |
| commit | bac5b32b47848b777ff7bd0b0ca7212d6d8dba9a (patch) | |
| tree | 6bbacd8afd1c143356d75564afeef62ee33735be | |
| parent | 163e1109fb267c0580425ae167cae12b9fd28680 (diff) | |
| download | pyramid-bac5b32b47848b777ff7bd0b0ca7212d6d8dba9a.tar.gz pyramid-bac5b32b47848b777ff7bd0b0ca7212d6d8dba9a.tar.bz2 pyramid-bac5b32b47848b777ff7bd0b0ca7212d6d8dba9a.zip | |
Features
--------
- If a resource implements a ``__resource_url__`` method, it will be called
as the result of invoking the ``pyramid.url.resource_url`` function to
generate a URL, overriding the default logic. See the new "Generating The
URL Of A Resource" section within the Resources narrative chapter.
Documentation
-------------
- Added "Generating The URL Of A Resource" section to the Resources narrative
chapter (includes information about overriding URL generation using
``__resource_url__``).
- Added "Generating the Path To a Resource" section to the Resources
narrative chapter.
- Added "Finding a Resource by Path" section to the Resources narrative
chapter.
| -rw-r--r-- | CHANGES.txt | 20 | ||||
| -rw-r--r-- | docs/narr/resources.rst | 217 | ||||
| -rw-r--r-- | docs/narr/vhosting.rst | 2 | ||||
| -rw-r--r-- | pyramid/tests/test_traversal.py | 24 | ||||
| -rw-r--r-- | pyramid/traversal.py | 94 | ||||
| -rw-r--r-- | pyramid/url.py | 4 |
6 files changed, 308 insertions, 53 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index 362d66a90..b682f7119 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,6 +1,26 @@ Next release ============ +Features +-------- + +- If a resource implements a ``__resource_url__`` method, it will be called + as the result of invoking the ``pyramid.url.resource_url`` function to + generate a URL, overriding the default logic. See the new "Generating The + URL Of A Resource" section within the Resources narrative chapter. + +Documentation +------------- + +- Added "Generating The URL Of A Resource" section to the Resources narrative + chapter (includes information about overriding URL generation using + ``__resource_url__``). + +- Added "Generating the Path To a Resource" section to the Resources + narrative chapter. + +- Added "Finding a Resource by Path" section to the Resources narrative + chapter. 1.0a7 (2010-12-20) ================== diff --git a/docs/narr/resources.rst b/docs/narr/resources.rst index 2c0d4deba..da2001960 100644 --- a/docs/narr/resources.rst +++ b/docs/narr/resources.rst @@ -1,11 +1,13 @@ Resources ========= -A :term:`resource` is an object that represents a "place" in your -application. Every :app:`Pyramid` application has at least one resource -object: the :term:`root` resource. The root resource is the root of a -:term:`resource tree`. A resource tree is a set of nested dictionary-like -objects which you can use to represent your website's structure. +A :term:`resource` is an object that represents a "place" in a tree related +to your application. Every :app:`Pyramid` application has at least one +resource object: the :term:`root` resource (even if you don't define one +manually, a default root resource is created for you). The root resource is +the root of a :term:`resource tree`. A resource tree is a set of nested +dictionary-like objects which you can use to represent your website's +structure. In an application which uses :term:`traversal` to map URLs to code, the resource tree structure is used heavily to map a URL to a :term:`view @@ -150,11 +152,11 @@ retrieved from the container via ``__getitem__``. This pattern continues recursively "up" the tree from the root. The ``__parent__`` attributes of each resource form a linked list that points -"upward" toward the root. This is analogous to the `..` entry in filesystem -directories. If you follow the ``__parent__`` values from any resource in the -resource tree, you will eventually come to the root resource, just like if -you keep executing the ``cd ..`` filesystem command, eventually you will -reach the filesystem root directory. +"downwards" toward the root. This is analogous to the `..` entry in +filesystem directories. If you follow the ``__parent__`` values from any +resource in the resource tree, you will eventually come to the root resource, +just like if you keep executing the ``cd ..`` filesystem command, eventually +you will reach the filesystem root directory. .. warning:: If your root resource has a ``__name__`` argument that is not ``None`` or the empty string, URLs returned by the @@ -199,8 +201,199 @@ and (usually) :func:`~pyramid.security.has_permission` and In general, since so much :app:`Pyramid` infrastructure depends on location-aware resources, it's a good idea to make each resource in your tree -location-aware, even though location-awareness is not a prerequisite for -plain traversal. +location-aware. + +.. index:: + single: resource_url + pair: generating; resource url + +Generating The URL Of A Resource +-------------------------------- + +If your resources are :term:`location` aware, you can use the +:func:`pyramid.url.resource_url` API to generate a URL for the resource. +This URL will use the resource's position in the parent tree to create a +resource path, and it will prefix the path with the current application URL +to form a fully-qualified URL with the scheme, host, port, and path. You can +also pass extra arguments to :func:`~pyramid.url.resource_url` to influence +the generated URL. + +The simplest call to :func:`~pyramid.url.resource_url` looks like this: + +.. code-block:: python + :linenos: + + from pyramid.url import resource_url + url = resource_url(resource, request) + +The ``request`` passed to ``resource_url`` in the above example is an +instance of an :app:`Pyramid` :term:`request` object. + +If the resource referred to as ``resource`` in the above example was the root +resource, and the host that was used to contact the server was +``example.com``, the URL generated would be ``http://example.com/``. +However, if the resource was a child of the root resource named ``a``, the +generated URL would be ``http://example.com/a/``. + +A slash is appended to all resource URLs when +:func:`~pyramid.url.resource_url` is used to generate them in this simple +manner, because resources are "places" in the hierarchy, and URLs are meant +to be clicked on to be visited. Relative URLs that you include on HTML pages +rendered as the result of the default view of a resource are typically more +apt to be relative to these resources than relative to their parent. + +You can also pass extra elements to :func:`~pyramid.url.resource_url`: + +.. code-block:: python + :linenos: + + from pyramid.url import resource_url + url = resource_url(resource, request, 'foo', 'bar') + +If the resource referred to as ``resource`` in the above example was the root +resource, and the host that was used to contact the server was +``example.com``, the URL generated would be ``http://example.com/foo/bar``. +Any number of extra elements can be passed to +:func:`~pyramid.url.resource_url` as extra positional arguments. When extra +elements are passed, they are appended to the resource's URL. A slash is not +appended to the final segment when elements are passed. + +You can also pass a query string: + +.. code-block:: python + :linenos: + + from pyramid.url import resource_url + url = resource_url(resource, request, query={'a':'1'}) + +If the resource referred to as ``resource`` in the above example was the root +resource, and the host that was used to contact the server was +``example.com``, the URL generated would be ``http://example.com/?a=1``. + +When a :term:`virtual root` is active, the URL generated by +:func:`~pyramid.url.resource_url` for a resource may be "shorter" than its +physical tree path. See :ref:`virtual_root_support` for more information +about virtually rooting a resource. + +The shortcut method of the :term:`request` named +:meth:`pyramid.request.Request.resource_url` can be used instead of +:func:`pyramid.url.resource_url` to generate a resource URL. + +For more information about generating resource URLs, see the documentation +for :func:`pyramid.url.resource_url`. + +.. _overriding_resource_url_generation: + +Overriding Resource URL Generation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If a resource object implements a ``__resource_url__`` method, this method +will be called when :func:`pyramid.url.resource_url` is called to generate a +URL for the resource, overriding the default URL returned for the resource by +:func:`~pyramid.url.resource_url`. + +The ``__resource_url__`` hook is passed two arguments: ``request`` and +``info``. ``request`` is the :term:`request` object passed to +:func:`pyramid.url.resource_url`. ``info`` is a dictionary with two +keys: + +``physical_path`` + The "physical path" computed for the resource, as defined by + ``pyramid.traversal.resource_path(resource)``. + +``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. + +The ``__resource_url__`` method of a resource should return a string +representing a URL. If it cannot override the default, it should return +``None``. If it returns ``None``, the default URL will be returned. + +Here's an example ``__resource_url__`` method. + +.. code-block:: python + :linenos: + + class Resource(object): + def __resource_url__(self, request, info): + return request.application_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. + +Generating the Path To a Resource +--------------------------------- + +:func:`pyramid.traversal.resource_path` returns a string object representing +the absolute physical path of the resource object based on its position in +the resource tree. Each segment of the path is separated with a slash +character. + +.. code-block:: python + :linenos: + + from pyramid.traversal import resource_path + url = resource_path(resource) + +If ``resource`` in the example above was accessible in the tree as +``root['a']['b']``, the above example would generate the string ``/a/b``. + +Any positional arguments passed in to :func:`pyramid.traversal.resource_path` +will be appended as path segments to the end of the resource path. + +.. code-block:: python + :linenos: + + from pyramid.traversal import resource_path + url = resource_path(resource, 'foo', 'bar') + +If ``resource`` in the example above was accessible in the tree as +``root['a']['b']``, the above example would generate the string +``/a/b/foo/bar``. + +The resource passed in must be :term:`location`-aware. + +The presence or absence of a :term:`virtual root` has no impact on the +behavior of :func:`~pyramid.traversal.resource_path`. + +Finding a Resource by Path +-------------------------- + +If you have a string path to a resource, you can grab the resource from +that place in the application's resource tree using +:func:`pyramid.traversal.find_resource`. + +You can resolve an absolute path by passing a string prefixed with a ``/`` as +the ``path`` argument: + +.. code-block:: python + :linenos: + + from pyramid.traversal import find_resource + url = find_resource(anyresource, '/path') + +Or you can resolve a path relative to the resource you pass in by passing a +string that isn't prefixed by ``/``: + +.. code-block:: python + :linenos: + + from pyramid.traversal import find_resource + url = find_resource(anyresource, 'path') + +Often the paths you pass to :func:`~pyramid.traversal.find_resource` are +generated by the :func:`~pyramid.traversal.resource_path` API. These APIs +are "mirrors" of each other. + +If the path cannot be resolved when calling +:func:`~pyramid.traversal.find_resource` (if the respective resource in the +tree does not exist), a :exc:`KeyError` will be raised. + +See the :func:`pyramid.traversal.find_resource` documentation for more +information about resolving a path to a resource. .. index:: single: resource interfaces diff --git a/docs/narr/vhosting.rst b/docs/narr/vhosting.rst index 65168806e..d3ff260e3 100644 --- a/docs/narr/vhosting.rst +++ b/docs/narr/vhosting.rst @@ -78,6 +78,8 @@ In the above configuration, we root a :app:`Pyramid` application at .. index:: single: virtual root +.. _virtual_root_support: + Virtual Root Support -------------------- diff --git a/pyramid/tests/test_traversal.py b/pyramid/tests/test_traversal.py index df9e1e554..147e53702 100644 --- a/pyramid/tests/test_traversal.py +++ b/pyramid/tests/test_traversal.py @@ -829,6 +829,30 @@ class TraversalContextURLTests(unittest.TestCase): result = context_url() self.assertEqual(result, 'http://example.com:5432//bar/') + def test_local_url_returns_None(self): + resource = DummyContext() + def resource_url(request, info): + self.assertEqual(info['virtual_path'], '/') + self.assertEqual(info['physical_path'], '/') + return None + resource.__resource_url__ = resource_url + request = DummyRequest() + context_url = self._makeOne(resource, request) + result = context_url() + self.assertEqual(result, 'http://example.com:5432/') + + def test_local_url_returns_url(self): + resource = DummyContext() + def resource_url(request, info): + self.assertEqual(info['virtual_path'], '/') + self.assertEqual(info['physical_path'], '/') + return 'abc' + resource.__resource_url__ = resource_url + request = DummyRequest() + context_url = self._makeOne(resource, request) + result = context_url() + self.assertEqual(result, 'abc') + class TestVirtualRoot(unittest.TestCase): def setUp(self): cleanUp() diff --git a/pyramid/traversal.py b/pyramid/traversal.py index 086823695..73878022c 100644 --- a/pyramid/traversal.py +++ b/pyramid/traversal.py @@ -16,7 +16,7 @@ from pyramid.location import lineage from pyramid.threadlocal import get_current_registry def find_root(resource): - """ Find the root node in the graph to which ``resource`` + """ Find the root node in the resource tree to which ``resource`` belongs. Note that ``resource`` should be :term:`location`-aware. Note that the root resource is available in the request object by accessing the ``request.root`` attribute. @@ -29,13 +29,12 @@ def find_root(resource): def find_resource(resource, path): """ Given a resource object and a string or tuple representing a path - (such as the return value of - :func:`pyramid.traversal.resource_path` or - :func:`pyramid.traversal.resource_path_tuple`), return a context - in this application's resource tree at the specified path. The - resource passed in *must* be :term:`location`-aware. If the path - cannot be resolved (if the respective node in the graph does not - exist), a :exc:`KeyError` will be raised. + (such as the return value of :func:`pyramid.traversal.resource_path` or + :func:`pyramid.traversal.resource_path_tuple`), return a resource in this + application's resource tree at the specified path. The resource passed + in *must* be :term:`location`-aware. If the path cannot be resolved (if + the respective node in the resource tree does not exist), a + :exc:`KeyError` will be raised. This function is the logical inverse of :func:`pyramid.traversal.resource_path` and @@ -44,10 +43,10 @@ def find_resource(resource, path): Rules for passing a *string* as the ``path`` argument: if the first character in the path string is the with the ``/`` - character, the path will considered absolute and the graph + character, the path will considered absolute and the resource tree traversal will start at the root object. If the first character of the path string is *not* the ``/`` character, the path is - considered relative and graph traversal will begin at the resource + considered relative and resource tree traversal will begin at the resource object supplied to the function as the ``resource`` argument. If an empty string is passed as ``path``, the ``resource`` passed in will be returned. Resource path strings must be escaped in the following @@ -60,10 +59,10 @@ def find_resource(resource, path): Rules for passing a *tuple* as the ``path`` argument: if the first element in the path tuple is the empty string (for example ``('', - 'a', 'b', 'c')``, the path is considered absolute and the graph - traversal will start at the graph root object. If the first + 'a', 'b', 'c')``, the path is considered absolute and the resource tree + traversal will start at the resource tree root object. If the first element in the path tuple is not the empty string (for example - ``('a', 'b', 'c')``), the path is considered relative and graph + ``('a', 'b', 'c')``), the path is considered relative and resource tree traversal will begin at the resource object supplied to the function as the ``resource`` argument. If an empty sequence is passed as ``path``, the ``resource`` passed in itself will be returned. No @@ -126,7 +125,7 @@ def resource_path(resource, *elements): .. note:: Each segment in the path string returned will use the ``__name__`` attribute of the resource it represents within - the graph. Each of these segments *should* be a unicode + the resource tree. Each of these segments *should* be a unicode or string object (as per the contract of :term:`location`-awareness). However, no conversion or safety checking of resource names is performed. For @@ -207,7 +206,7 @@ def traverse(resource, path): via :term:`url dispatch`, traversed will be None. - ``virtual_root``: A resource object representing the 'virtual' root - of the object graph being traversed during :term:`traversal`. + of the resource tree being traversed during :term:`traversal`. See :ref:`vhosting_chapter` for a definition of the virtual root object. If no virtual hosting is in effect, and the ``path`` passed in was absolute, the ``virtual_root`` will be the @@ -230,10 +229,10 @@ def traverse(resource, path): Rules for passing a *string* as the ``path`` argument: if the first character in the path string is the with the ``/`` - character, the path will considered absolute and the graph + character, the path will considered absolute and the resource tree traversal will start at the root object. If the first character of the path string is *not* the ``/`` character, the path is - considered relative and graph traversal will begin at the resource + considered relative and resource tree traversal will begin at the resource object supplied to the function as the ``resource`` argument. If an empty string is passed as ``path``, the ``resource`` passed in will be returned. Resource path strings must be escaped in the following @@ -246,10 +245,10 @@ def traverse(resource, path): Rules for passing a *tuple* as the ``path`` argument: if the first element in the path tuple is the empty string (for example ``('', - 'a', 'b', 'c')``, the path is considered absolute and the graph - traversal will start at the graph root object. If the first + 'a', 'b', 'c')``, the path is considered absolute and the resource tree + traversal will start at the resource tree root object. If the first element in the path tuple is not the empty string (for example - ``('a', 'b', 'c')``), the path is considered relative and graph + ``('a', 'b', 'c')``), the path is considered relative and resource tree traversal will begin at the resource object supplied to the function as the ``resource`` argument. If an empty sequence is passed as ``path``, the ``resource`` passed in itself will be returned. No @@ -313,7 +312,7 @@ def traverse(resource, path): def resource_path_tuple(resource, *elements): """ Return a tuple representing the absolute physical path of the - ``resource`` object based on its position in an object graph, e.g + ``resource`` object based on its position in a resource tree, e.g ``('', 'foo', 'bar')``. Any positional arguments passed in as ``elements`` will be appended as elements in the tuple representing the resource path. For instance, if the resource's @@ -331,7 +330,7 @@ def resource_path_tuple(resource, *elements): .. note:: Each segment in the path tuple returned will equal the ``__name__`` attribute of the resource it represents within - the graph. Each of these segments *should* be a unicode + the resource tree. Each of these segments *should* be a unicode or string object (as per the contract of :term:`location`-awareness). However, no conversion or safety checking of resource names is performed. For @@ -379,7 +378,7 @@ def virtual_root(resource, request): API will be used to find the virtual root object using this path; if the object is found, it will be returned. If the ``HTTP_X_VHM_ROOT`` key is is not present in the WSGI environment, - the physical :term:`root` of the graph will be returned instead. + the physical :term:`root` of the resource tree will be returned instead. Virtual roots are not useful at all in applications that use :term:`URL dispatch`. Contexts obtained via URL dispatch don't @@ -401,7 +400,7 @@ def virtual_root(resource, request): def traversal_path(path): """ Given a ``PATH_INFO`` string (slash-separated path segments), return a tuple representing that path which can be used to - traverse a graph. + traverse a resource tree. The ``PATH_INFO`` is split on slashes, creating a list of segments. Each segment is URL-unquoted, and subsequently decoded @@ -657,30 +656,43 @@ class TraversalContextURL(object): return find_root(self.context) def __call__(self): - """ Generate a URL based on the :term:`lineage` of a - :term:`resource` object obtained via :term:`traversal`. If any - resource in the context lineage has a Unicode name, it will be - converted to a UTF-8 string before being attached to the URL. - If a ``HTTP_X_VHM_ROOT`` key is present in the WSGI - environment, its value will be treated as a 'virtual root - path': the path of the URL generated by this will be + """ Generate a URL based on the :term:`lineage` of a :term:`resource` + object that is ``self.context``. If any resource in the context + lineage has a Unicode name, it will be converted to a UTF-8 string + before being attached to the URL. If a ``HTTP_X_VHM_ROOT`` key is + present in the WSGI environment, its value will be treated as a + 'virtual root path': the path of the URL generated by this will be left-stripped of this virtual root path value. """ - path = resource_path(self.context) - if path != '/': - path = path + '/' + 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 - - # if the path starts with the virtual root path, trim it out - if vroot_varname in environ: - vroot_path = environ[vroot_varname] - if path.startswith(vroot_path): - path = path[len(vroot_path):] + 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) + if local_url is not None: + result = local_url(request, + {'virtual_path':virtual_path, + 'physical_path':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 + path + return app_url + virtual_path @lru_cache(1000) def _join_path_tuple(tuple): diff --git a/pyramid/url.py b/pyramid/url.py index 6ae0ce8c4..4c2f5d393 100644 --- a/pyramid/url.py +++ b/pyramid/url.py @@ -255,6 +255,10 @@ def resource_url(resource, request, *elements, **kw): will always follow the query element, e.g. ``http://example.com?foo=1#bar``. + If the ``resource`` passed in has a ``__resource_url__`` method, it will + be used to generate the URL that is returned by this function. See also + :ref:`overriding_resource_url_generation`. + .. note:: If the :term:`resource` used is the result of a :term:`traversal`, it must be :term:`location`-aware. The resource can also be the context of a :term:`URL |
