From 35b570451606c02ebb0dd4cac8f0cebc9a8b1c29 Mon Sep 17 00:00:00 2001
From: Pepe Osca <pposca@gmail.com>
Date: Thu, 10 May 2018 20:55:26 +0200
Subject: [PATCH] Add canonical property to routes and resources

The canonical is the path used to add a new route. For example,
/foo/bar/{name}. For DynamicResource, canonical exposes the
formatter.

 - Add canonical property to AbstractRoute and AbstractResource
 - Add canonical implementation to PlainResource, DynamicResource,
 PrefixResource, StaticResource, ResourceRoute and SystemRoute.
 - Add tests

Closes #2968
---
 CHANGES/2968.feature         |  1 +
 CONTRIBUTORS.txt             |  1 +
 aiohttp/web_urldispatcher.py | 42 ++++++++++++++++++++++++++
 tests/test_urldispatch.py    | 58 +++++++++++++++++++++++++++++++++++-
 4 files changed, 101 insertions(+), 1 deletion(-)
 create mode 100644 CHANGES/2968.feature

diff --git a/CHANGES/2968.feature b/CHANGES/2968.feature
new file mode 100644
index 00000000000..52a3a9d766a
--- /dev/null
+++ b/CHANGES/2968.feature
@@ -0,0 +1 @@
+Add canonical property to routes and resources
diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt
index f29139e747f..b066aa15a5e 100644
--- a/CONTRIBUTORS.txt
+++ b/CONTRIBUTORS.txt
@@ -153,6 +153,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..e16d7f2b4dd 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 route's canonical.
+
+        For example '/foo/bar/{name}'
+
+        """
+
     @abc.abstractmethod  # pragma: no branch
     def url_for(self, **kwargs):
         """Construct url for resource with additional params."""
@@ -131,6 +140,15 @@ def handler(self):
     def name(self):
         """Optional route's name, always equals to resource's name."""
 
+    @property
+    @abc.abstractmethod
+    def canonical(self):
+        """Exposes the route's canonical.
+
+        For example '/foo/bar/{name}'
+
+        """
+
     @property
     def resource(self):
         return self._resource
@@ -300,6 +318,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 +395,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 +440,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('/')
@@ -457,6 +487,10 @@ def __init__(self, prefix, directory, *, name=None,
                         'HEAD': ResourceRoute('HEAD', self._handle, self,
                                               expect_handler=expect_handler)}
 
+    @property
+    def canonical(self):
+        return self._prefix + str(self._directory)
+
     def url_for(self, *, filename, append_version=None):
         if append_version is None:
             append_version = self._append_version
@@ -669,6 +703,10 @@ def __repr__(self):
     def name(self):
         return self._resource.name
 
+    @property
+    def canonical(self):
+        return self._resource.canonical
+
     def url_for(self, *args, **kwargs):
         """Construct url for route with additional params."""
         return self._resource.url_for(*args, **kwargs)
@@ -690,6 +728,10 @@ def url_for(self, *args, **kwargs):
     def name(self):
         return None
 
+    @property
+    def canonical(self):
+        return None
+
     def get_info(self):
         return {'http_exception': self._http_exception}
 
diff --git a/tests/test_urldispatch.py b/tests/test_urldispatch.py
index 293225d3e60..c8daf66e919 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,57 @@ 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 + directory
+    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
+
+
+def test_resource_route_canonical(router):
+    canonical = '/plain'
+    route = router.add_route('GET', canonical, make_handler())
+    assert route.canonical == canonical
+
+    canonical = '/variable/{name}'
+    route = router.add_route('GET', canonical, make_handler())
+    assert route.canonical == canonical
+
+    prefix = '/prefix'
+    directory = str(os.path.dirname(aiohttp.__file__))
+    canonical = prefix + directory
+    route = router.add_static(prefix, directory)
+    assert route.canonical == canonical
+
+
+def test_system_route_canonical():
+    route = SystemRoute(BaseException())
+    assert route.canonical is None