diff --git a/CHANGES/2968.feature b/CHANGES/2968.feature new file mode 100644 index 00000000000..1803b2a30c0 --- /dev/null +++ b/CHANGES/2968.feature @@ -0,0 +1 @@ +Add canonical property to resources diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 5e7394d0dff..c9f32f9866c 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -154,6 +154,7 @@ Paul Colomiets Paulus Schoutsen Pavel Kamaev Pawel Miech +Pepe Osca Philipp A. Pieter van Beek Rafael Viotti diff --git a/aiohttp/web_urldispatcher.py b/aiohttp/web_urldispatcher.py index 16cf008986d..6b3f58e7e2f 100644 --- a/aiohttp/web_urldispatcher.py +++ b/aiohttp/web_urldispatcher.py @@ -45,6 +45,15 @@ def __init__(self, *, name=None): def name(self): return self._name + @property + @abc.abstractmethod + def canonical(self): + """Exposes the resource's canonical path. + + For example '/foo/bar/{name}' + + """ + @abc.abstractmethod # pragma: no branch def url_for(self, **kwargs): """Construct url for resource with additional params.""" @@ -300,6 +309,10 @@ def __init__(self, path, *, name=None): assert not path or path.startswith('/') self._path = path + @property + def canonical(self): + return self._path + def freeze(self): if not self._path: self._path = '/' @@ -373,6 +386,10 @@ def __init__(self, path, *, name=None): self._pattern = compiled self._formatter = formatter + @property + def canonical(self): + return self._formatter + def add_prefix(self, prefix): assert prefix.startswith('/') assert not prefix.endswith('/') @@ -414,6 +431,10 @@ def __init__(self, prefix, *, name=None): super().__init__(name=name) self._prefix = URL.build(path=prefix).raw_path + @property + def canonical(self): + return self._prefix + def add_prefix(self, prefix): assert prefix.startswith('/') assert not prefix.endswith('/') diff --git a/docs/web_reference.rst b/docs/web_reference.rst index e1cf79f7bc9..29a3c944d32 100644 --- a/docs/web_reference.rst +++ b/docs/web_reference.rst @@ -1826,6 +1826,13 @@ Resource classes hierarchy:: Read-only *name* of resource or ``None``. + .. attribute:: canonical + + Read-only *canonical path* associate with the resource. For example + ``/path/to`` or ``/path/{to}`` + + .. versionadded:: 3.3 + .. comethod:: resolve(request) Resolve resource by finding appropriate :term:`web-handler` for @@ -1889,6 +1896,12 @@ Resource classes hierarchy:: The class corresponds to resources with plain-text matching, ``'/path/to'`` for example. + .. attribute:: canonical + + Read-only *canonical path* associate with the resource. Returns the path + used to create the PlainResource. For example ``/path/to`` + + .. versionadded:: 3.3 .. method:: url_for() @@ -1903,6 +1916,13 @@ Resource classes hierarchy:: :ref:`variable ` matching, e.g. ``'/path/{to}/{param}'`` etc. + .. attribute:: canonical + + Read-only *canonical path* associate with the resource. Returns the + formatter obtained from the path used to create the DynamicResource. + For example, from a path ``/get/{num:^\d+}``, it returns ``/get/{num}`` + + .. versionadded:: 3.3 .. method:: url_for(**params) @@ -1921,6 +1941,13 @@ Resource classes hierarchy:: The class corresponds to resources for :ref:`static file serving `. + .. attribute:: canonical + + Read-only *canonical path* associate with the resource. Returns the prefix + used to create the StaticResource. For example ``/prefix`` + + .. versionadded:: 3.3 + .. method:: url_for(filename, append_version=None) Returns a :class:`~yarl.URL` for file path under resource prefix. @@ -1948,6 +1975,14 @@ Resource classes hierarchy:: A resource for serving nested applications. The class instance is returned by :class:`~aiohttp.web.Application.add_subapp` call. + .. attribute:: canonical + + Read-only *canonical path* associate with the resource. Returns the + prefix used to create the PrefixedSubAppResource. + For example ``/prefix`` + + .. versionadded:: 3.3 + .. method:: url_for(**kwargs) The call is not allowed, it raises :exc:`RuntimeError`. diff --git a/tests/test_urldispatch.py b/tests/test_urldispatch.py index 293225d3e60..f3cdde47d40 100644 --- a/tests/test_urldispatch.py +++ b/tests/test_urldispatch.py @@ -12,7 +12,9 @@ from aiohttp.test_utils import make_mocked_request from aiohttp.web import HTTPMethodNotAllowed, HTTPNotFound, Response from aiohttp.web_urldispatcher import (PATH_SEP, AbstractResource, - ResourceRoute, SystemRoute, View, + DynamicResource, PlainResource, + ResourceRoute, StaticResource, + SystemRoute, View, _default_expect_handler) @@ -1112,3 +1114,36 @@ def handler(request): with pytest.warns(DeprecationWarning): router.add_route('GET', '/handler', handler) + + +def test_plain_resource_canonical(): + canonical = '/plain/path' + res = PlainResource(path=canonical) + assert res.canonical == canonical + + +def test_dynamic_resource_canonical(): + canonicals = { + '/get/{name}': '/get/{name}', + '/get/{num:^\d+}': '/get/{num}', + r'/handler/{to:\d+}': r'/handler/{to}', + r'/{one}/{two:.+}': r'/{one}/{two}', + } + for pattern, canonical in canonicals.items(): + res = DynamicResource(path=pattern) + assert res.canonical == canonical + + +def test_static_resource_canonical(): + prefix = '/prefix' + directory = str(os.path.dirname(aiohttp.__file__)) + canonical = prefix + res = StaticResource(prefix=prefix, directory=directory) + assert res.canonical == canonical + + +def test_prefixed_subapp_resource_canonical(app, loop): + canonical = '/prefix' + subapp = web.Application() + res = subapp.add_subapp(canonical, subapp) + assert res.canonical == canonical