Repository: level12/keg

Issue:
rule() when used w/ a relative URL should not result in the default URL also being available
```python

class ApprovalReview(ApprovalBaseView):
    rule('<int:week_id>', post=True)

    def get(self, week_id):
        pass

```

```
$ marshal develop routes | grep review
admin.approval-review                         GET,OPTIONS,HEAD,POST   /admin/approval-review
admin.approval-review                         GET,OPTIONS,HEAD,POST   /admin/approval-review/<int:week_id>
```

I was expecting only the second route in the list above to be generated.  I think the first route being present is a bug.

Discussion:

Patch:
diff --git a/docs/source/readme/installation.rst b/docs/source/readme/installation.rst
index 2c8a4e6..97859b6 100644
--- a/docs/source/readme/installation.rst
+++ b/docs/source/readme/installation.rst
@@ -2,3 +2,25 @@ Installation
 ============
 
 ``pip install keg``
+
+
+Upgrade Notes
+=============
+
+While we attempt to preserve backward compatibility, some Keg versions do introduce
+breaking changes. This list should provide information on needed app changes.
+
+- 0.10.0
+  - ``rule``: default class view route no longer generated when any rules are present
+
+    - Absolute route had been provided automatically from the class name, but in some situations
+    this would not be desired. Views that still need that route can use a couple of solutions:
+
+      - Provide an absolute route rule: ``rule('/my-route')``
+      - Use an empty relative route rule: ``rule()``
+    
+    - All of an app's routes may be shown on CLI with the ``<app> develop routes`` command
+
+  - Removed ``keg`` blueprint along with ``ping`` and ``exception-test`` routes
+  - DB manager ``prep_empty`` method no longer called (had been deprecated)
+  - Python 2 support removed
diff --git a/keg/web.py b/keg/web.py
index a6b09dc..cae3c59 100644
--- a/keg/web.py
+++ b/keg/web.py
@@ -315,16 +315,16 @@ class BaseView(MethodView, metaclass=_ViewMeta):
             str_endpoint = str(endpoint)
             view_func = cls.as_view(str_endpoint)
 
-            absolute_found = False
             for rule, options in rules:
-                if rule.startswith('/'):
-                    absolute_found = True
+                if rule and rule.startswith('/'):
                     class_url = rule
+                elif not rule:
+                    rule = class_url
                 else:
                     rule = '{}/{}'.format(class_url, rule)
                 cls.blueprint.add_url_rule(rule, endpoint=endpoint, view_func=view_func, **options)
 
-            if not absolute_found:
+            if not rules:
                 cls.blueprint.add_url_rule(class_url, endpoint=endpoint, view_func=view_func)
 
         for rule, options in cls.url_rules:
diff --git a/keg_apps/web/views/routing.py b/keg_apps/web/views/routing.py
index f2dc75c..b625291 100644
--- a/keg_apps/web/views/routing.py
+++ b/keg_apps/web/views/routing.py
@@ -60,10 +60,10 @@ class ExplicitRouteAlt(KegBaseView):
 
 class HelloWorld(BaseView):
     """
-        /hello -> 'Hello World'
-        /hello/foo -> 'Hello Foo'
+        /hello-world -> 'Hello World'
+        /hello-world/foo -> 'Hello Foo'
     """
-    # relative URL indicates this route should be appended to the default rule for the class
+    rule()
     rule('<name>')
 
     def get(self, name='World'):
@@ -101,6 +101,18 @@ class HelloReq(BaseView):
         return _('Hello {name}', name=name)
 
 
+class HelloReq2(BaseView):
+    """
+        /hello-req2 -> 404
+        /hello-req2/foo -> 'Hello Foo'
+    """
+    # no absolute rule, but only one endpoint to use
+    rule('<name>')
+
+    def get(self, name):
+        return _('Hello {name}', name=name)
+
+
 class Cars(BaseView):
     """
         CRUD for a model/entity


Test patch:
diff --git a/keg/tests/test_view_routing.py b/keg/tests/test_view_routing.py
index 03fb8ed..84f069a 100644
--- a/keg/tests/test_view_routing.py
+++ b/keg/tests/test_view_routing.py
@@ -54,6 +54,7 @@ class TestViewRouting(WebBase):
             'routing.cars:list',
             'routing.explicit-route',
             'routing.hello-req',
+            'routing.hello-req2',
             'routing.hello-world',
             'routing.hw-rule-default',
             'routing.misc',
@@ -210,6 +211,20 @@ class TestViewRouting(WebBase):
         assert rule.methods == {'GET', 'HEAD', 'OPTIONS'}
         assert rule.endpoint == 'routing.hello-req'
 
+    def test_route_no_absolute_single_endpoint(self):
+        self.testapp.get('/hello-req2', status=404)
+
+        resp = self.testapp.get('/hello-req2/foo')
+        assert resp.text == 'Hello foo'
+
+        rules = list(self.app.url_map.iter_rules(endpoint='routing.hello-req2'))
+        assert len(rules) == 1
+        rule = rules.pop()
+
+        assert rule.rule == '/hello-req2/<name>'
+        assert rule.methods == {'GET', 'HEAD', 'OPTIONS'}
+        assert rule.endpoint == 'routing.hello-req2'
+
     def test_route_plain(self):
         resp = self.testapp.get('/cars/list')
         assert resp.text == 'list'