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.

Comments:

Patch:
diff --git a/docs/source/index.rst b/docs/source/index.rst
index 4a07fac..7b9caf0 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -25,3 +25,4 @@ Contents
    web
    readme/features
    readme/configuration
+   readme/internationalization
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/docs/source/readme/internationalization.rst b/docs/source/readme/internationalization.rst
new file mode 100644
index 0000000..7a18fc6
--- /dev/null
+++ b/docs/source/readme/internationalization.rst
@@ -0,0 +1,18 @@
+Internationalization
+====================
+
+Keg can optionally be installed with the ``morphi`` library to use ``babel`` for internationalization::
+
+    pip install keg[i18n]
+
+The ``setup.cfg`` file is configured to handle the standard message extraction commands. For ease of development
+and ensuring that all marked strings have translations, a tox environment is defined for testing i18n. This will
+run commands to update and compile the catalogs, and specify any strings which need to be added.
+
+The desired workflow here is to run tox, update strings in the PO files as necessary, run tox again
+(until it passes), and then commit the changes to the catalog files.
+
+.. code::
+
+    tox -e i18n
+
diff --git a/keg/app.py b/keg/app.py
index abf0b5c..c6519b9 100644
--- a/keg/app.py
+++ b/keg/app.py
@@ -3,7 +3,6 @@ import importlib
 import flask
 from werkzeug.datastructures import ImmutableDict
 
-from keg.blueprints import keg as kegbp
 import keg.cli
 import keg.config
 from keg.ctx import KegRequestContext
@@ -137,8 +136,6 @@ class Keg(flask.Flask):
             comp_object.init_app(self, parent_path=comp_path)
 
     def init_blueprints(self):
-        # TODO: probably want to be selective about adding our blueprint
-        self.register_blueprint(kegbp)
         for blueprint in self.use_blueprints:
             self.register_blueprint(blueprint)
 
diff --git a/keg/blueprints/__init__.py b/keg/blueprints/__init__.py
deleted file mode 100644
index 70d297c..0000000
--- a/keg/blueprints/__init__.py
+++ /dev/null
@@ -1,16 +0,0 @@
-import flask
-
-from keg.extensions import lazy_gettext as _
-
-
-keg = flask.Blueprint('keg', __name__)
-
-
-@keg.route('/exception-test')
-def exception_test():
-    raise Exception(_('Deliberate exception for testing purposes'))
-
-
-@keg.route('/ping')
-def ping():
-    return '{} ok'.format(flask.current_app.name)
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'