diff --git a/flask/app.py b/flask/app.py index 3c479df5..1056aab8 100644 --- a/flask/app.py +++ b/flask/app.py @@ -945,11 +945,15 @@ class Flask(_PackageBoundObject): subdomain matching is in use. :param strict_slashes: can be used to disable the strict slashes setting for this rule. See above. + :param endpoint: Since version 0.8 you can also pass the enpoint, + it will be used instead of generating the endpoint + from the function name. :param options: other options to be forwarded to the underlying :class:`~werkzeug.routing.Rule` object. """ def decorator(f): - self.add_url_rule(rule, None, f, **options) + endpoint = options.pop("endpoint", None) + self.add_url_rule(rule, endpoint, f, **options) return f return decorator diff --git a/flask/blueprints.py b/flask/blueprints.py index 075961ab..8bfc610e 100644 --- a/flask/blueprints.py +++ b/flask/blueprints.py @@ -157,7 +157,8 @@ class Blueprint(_PackageBoundObject): :func:`url_for` function is prefixed with the name of the blueprint. """ def decorator(f): - self.add_url_rule(rule, f.__name__, f, **options) + endpoint = options.pop("endpoint", f.__name__) + self.add_url_rule(rule, endpoint, f, **options) return f return decorator @@ -165,6 +166,8 @@ class Blueprint(_PackageBoundObject): """Like :meth:`Flask.add_url_rule` but for a blueprint. The endpoint for the :func:`url_for` function is prefixed with the name of the blueprint. """ + if endpoint: + assert '.' not in endpoint, "Blueprint endpoint's should not contain dot's" self.record(lambda s: s.add_url_rule(rule, endpoint, view_func, **options)) diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py index 33975b99..078027b9 100644 --- a/flask/testsuite/basic.py +++ b/flask/testsuite/basic.py @@ -895,6 +895,32 @@ class BasicFunctionalityTestCase(FlaskTestCase): rv = c.post('/foo', data={}, follow_redirects=True) self.assert_equal(rv.data, 'success') + def test_route_decorator_custom_endpoint(self): + app = flask.Flask(__name__) + app.debug = True + + @app.route('/foo/') + def foo(): + return flask.request.endpoint + + @app.route('/bar/', endpoint='bar') + def for_bar(): + return flask.request.endpoint + + @app.route('/bar/123', endpoint='123') + def for_bar_foo(): + return flask.request.endpoint + + with app.test_request_context(): + assert flask.url_for('foo') == '/foo/' + assert flask.url_for('bar') == '/bar/' + assert flask.url_for('123') == '/bar/123' + + c = app.test_client() + self.assertEqual(c.get('/foo/').data, 'foo') + self.assertEqual(c.get('/bar/').data, 'bar') + self.assertEqual(c.get('/bar/123').data, '123') + class ContextTestCase(FlaskTestCase): diff --git a/flask/testsuite/blueprints.py b/flask/testsuite/blueprints.py index 88e2be36..73577961 100644 --- a/flask/testsuite/blueprints.py +++ b/flask/testsuite/blueprints.py @@ -415,6 +415,92 @@ class BlueprintTestCase(FlaskTestCase): self.assert_equal(c.get('/').data, '1') self.assert_equal(c.get('/page/2').data, '2') + def test_route_decorator_custom_endpoint(self): + + bp = flask.Blueprint('bp', __name__) + + @bp.route('/foo') + def foo(): + return flask.request.endpoint + + @bp.route('/bar', endpoint='bar') + def foo_bar(): + return flask.request.endpoint + + @bp.route('/bar/123', endpoint='123') + def foo_bar_foo(): + return flask.request.endpoint + + @bp.route('/bar/foo') + def bar_foo(): + return flask.request.endpoint + + app = flask.Flask(__name__) + app.register_blueprint(bp, url_prefix='/py') + + @app.route('/') + def index(): + return flask.request.endpoint + + c = app.test_client() + self.assertEqual(c.get('/').data, 'index') + self.assertEqual(c.get('/py/foo').data, 'bp.foo') + self.assertEqual(c.get('/py/bar').data, 'bp.bar') + self.assertEqual(c.get('/py/bar/123').data, 'bp.123') + self.assertEqual(c.get('/py/bar/foo').data, 'bp.bar_foo') + + def test_route_decorator_custom_endpoint_with_dots(self): + bp = flask.Blueprint('bp', __name__) + + @bp.route('/foo') + def foo(): + return flask.request.endpoint + + try: + @bp.route('/bar', endpoint='bar.bar') + def foo_bar(): + return flask.request.endpoint + except AssertionError: + pass + else: + raise AssertionError('expected AssertionError not raised') + + try: + @bp.route('/bar/123', endpoint='bar.123') + def foo_bar_foo(): + return flask.request.endpoint + except AssertionError: + pass + else: + raise AssertionError('expected AssertionError not raised') + + def foo_foo_foo(): + pass + + self.assertRaises( + AssertionError, + lambda: bp.add_url_rule( + '/bar/123', endpoint='bar.123', view_func=foo_foo_foo + ) + ) + + self.assertRaises( + AssertionError, + bp.route('/bar/123', endpoint='bar.123'), + lambda: None + ) + + app = flask.Flask(__name__) + app.register_blueprint(bp, url_prefix='/py') + + c = app.test_client() + self.assertEqual(c.get('/py/foo').data, 'bp.foo') + # The rule's din't actually made it through + rv = c.get('/py/bar') + assert rv.status_code == 404 + rv = c.get('/py/bar/123') + assert rv.status_code == 404 + def suite(): suite = unittest.TestSuite()