Repository: aio-libs/aiohttp

Issue:
Add web.static and web.RouteTableDef.static
As aliases for UrlDispatcher.add_static.

The idea is adding static file resources in declarative way: `app.add_routes([web.static(...)])` and `table.static(...)`.

Part of #2766


Discussion:

Resolving patch:
diff --git a/aiohttp/web_urldispatcher.py b/aiohttp/web_urldispatcher.py
--- a/aiohttp/web_urldispatcher.py
+++ b/aiohttp/web_urldispatcher.py
@@ -29,16 +29,23 @@
 __all__ = ('UrlDispatcher', 'UrlMappingMatchInfo',
            'AbstractResource', 'Resource', 'PlainResource', 'DynamicResource',
            'AbstractRoute', 'ResourceRoute',
-           'StaticResource', 'View', 'RouteDef', 'RouteTableDef',
-           'head', 'get', 'post', 'patch', 'put', 'delete', 'route', 'view')
+           'StaticResource', 'View', 'RouteDef', 'StaticDef', 'RouteTableDef',
+           'head', 'get', 'post', 'patch', 'put', 'delete', 'route', 'view',
+           'static')
 
 HTTP_METHOD_RE = re.compile(r"^[0-9A-Za-z!#\$%&'\*\+\-\.\^_`\|~]+$")
 ROUTE_RE = re.compile(r'(\{[_a-zA-Z][^{}]*(?:\{[^{}]*\}[^{}]*)*\})')
 PATH_SEP = re.escape('/')
 
 
+class AbstractRouteDef(abc.ABC):
+    @abc.abstractmethod
+    def register(self, router):
+        pass  # pragma: no cover
+
+
 @attr.s(frozen=True, repr=False, slots=True)
-class RouteDef:
+class RouteDef(AbstractRouteDef):
     method = attr.ib(type=str)
     path = attr.ib(type=str)
     handler = attr.ib()
@@ -61,6 +68,24 @@ def register(self, router):
                              **self.kwargs)
 
 
+@attr.s(frozen=True, repr=False, slots=True)
+class StaticDef(AbstractRouteDef):
+    prefix = attr.ib(type=str)
+    path = attr.ib(type=str)
+    kwargs = attr.ib()
+
+    def __repr__(self):
+        info = []
+        for name, value in sorted(self.kwargs.items()):
+            info.append(", {}={!r}".format(name, value))
+        return ("<StaticDef {prefix} -> {path}"
+                "{info}>".format(prefix=self.prefix, path=self.path,
+                                 info=''.join(info)))
+
+    def register(self, router):
+        router.add_static(self.prefix, self.path, **self.kwargs)
+
+
 class AbstractResource(Sized, Iterable):
 
     def __init__(self, *, name=None):
@@ -995,6 +1020,10 @@ def view(path, handler, **kwargs):
     return route(hdrs.METH_ANY, path, handler, **kwargs)
 
 
+def static(prefix, path, **kwargs):
+    return StaticDef(prefix, path, kwargs)
+
+
 class RouteTableDef(Sequence):
     """Route definition table"""
     def __init__(self):
@@ -1041,3 +1070,6 @@ def delete(self, path, **kwargs):
 
     def view(self, path, **kwargs):
         return self.route(hdrs.METH_ANY, path, **kwargs)
+
+    def static(self, prefix, path, **kwargs):
+        self._items.append(StaticDef(prefix, path, kwargs))


Test patch:
diff --git a/tests/test_route_def.py b/tests/test_route_def.py
--- a/tests/test_route_def.py
+++ b/tests/test_route_def.py
@@ -1,4 +1,7 @@
+import pathlib
+
 import pytest
+from yarl import URL
 
 from aiohttp import web
 from aiohttp.web_urldispatcher import UrlDispatcher
@@ -103,6 +106,19 @@ async def handler(request):
     assert str(route.url_for()) == '/'
 
 
+def test_static(router):
+    folder = pathlib.Path(__file__).parent
+    router.add_routes([web.static('/prefix', folder)])
+    assert len(router.resources()) == 1  # 2 routes: for HEAD and GET
+
+    resource = list(router.resources())[0]
+    info = resource.get_info()
+    assert info['prefix'] == '/prefix'
+    assert info['directory'] == folder
+    url = resource.url_for(filename='sample.key')
+    assert url == URL('/prefix/sample.key')
+
+
 def test_head_deco(router):
     routes = web.RouteTableDef()
 
@@ -256,6 +272,15 @@ async def handler(request):
     assert repr(rd) == "<RouteDef GET /path -> 'handler', extra='info'>"
 
 
+def test_repr_static_def():
+    routes = web.RouteTableDef()
+
+    routes.static('/prefix', '/path', name='name')
+
+    rd = routes[0]
+    assert repr(rd) == "<StaticDef /prefix -> /path, name='name'>"
+
+
 def test_repr_route_table_def():
     routes = web.RouteTableDef()