Repository: marcuxyz/mvc-flask (language: python)

Issue:
Refactory FlaskMVC class
It would be nice if we could refactor the FlaskMVC class including the blueprint calls to create the new routes.

This will make it easier to call hooks, such as:

- before_requests
- after_requests

and etc. Related by issue #10 


Discussion / hints:

Fix patch:
diff --git a/example/app.py b/example/app.py
deleted file mode 100644
index 9f53718..0000000
--- a/example/app.py
+++ /dev/null
@@ -1,12 +0,0 @@
-from flask import Flask
-from mvc_flask import FlaskMVC
-
-mvc = FlaskMVC()
-
-
-def create_app():
-    app = Flask(__name__)
-    app.config["FLASK_MVC_DIR"] = "example"
-    mvc.init_app(app)
-
-    return app
diff --git a/example/controllers/home_controller.py b/example/controllers/home_controller.py
deleted file mode 100644
index 09e186f..0000000
--- a/example/controllers/home_controller.py
+++ /dev/null
@@ -1,12 +0,0 @@
-from flask import render_template, redirect, url_for, request
-
-
-class HomeController:
-    def index(self):
-        return render_template("index.html")
-
-    def new(self):
-        return render_template("post/new.html")
-
-    def create(self):
-        return redirect(url_for(".index"))
diff --git a/example/views/index.html b/example/views/index.html
deleted file mode 100644
index 2d6a743..0000000
--- a/example/views/index.html
+++ /dev/null
@@ -1,1 +0,0 @@
-<h1>Hello, World!</h1>
\ No newline at end of file
diff --git a/example/views/post/new.html b/example/views/post/new.html
deleted file mode 100644
index b17b641..0000000
--- a/example/views/post/new.html
+++ /dev/null
@@ -1,12 +0,0 @@
-<form action="/create", method="POST">
-  <fieldset>
-    <legend>Formulate</legend>
-
-    <div>
-      <label for="">Name</label>
-      <input type="text" name="name" id="">
-    </div>
-    <input type="submit" value="Send">
-  </fieldset>
-
-</form>
\ No newline at end of file
diff --git a/mvc_flask/__init__.py b/mvc_flask/__init__.py
index 64f6ad8..ce88221 100644
--- a/mvc_flask/__init__.py
+++ b/mvc_flask/__init__.py
@@ -1,39 +1,55 @@
-import json
-from pathlib import Path
 from importlib import import_module
+import json
 
+from pathlib import Path
 from flask import Flask
+from flask.blueprints import Blueprint
+from collections import namedtuple
+
+
+Route = namedtuple("Route", ["method", "path", "controller", "action"])
 
 
 class FlaskMVC:
-    def __init__(self, app: Flask = None):
+    def __init__(self, app: Flask = None, directory: str = "app"):
+        self.directory = Path(directory)
+
         if app is not None:
             self.init_app(app)
 
     def init_app(self, app):
-        app.template_folder = "views"
-        app.config.setdefault("FLASK_MVC_DIR", "app")
-
-        self.root_path = Path(app.config["FLASK_MVC_DIR"])
-
-        self._register_router(app)
-
-    def _routes(self):
-        with open(self.root_path / "routes.json", mode="r") as f:
-            return json.load(f)
-
-    def _register_router(self, app):
-        for route in self._routes():
-            controller = route["controller"]
-
-            mod = import_module(
-                f"{self.root_path}.controllers.{controller}_controller"
+        self.create_blueprint(app)
+
+    def routes(self):
+        routes = []
+        with open(self.directory / "routes.json", mode="r") as f:
+            routes = [
+                Route(
+                    route["method"],
+                    route["path"],
+                    route["controller"],
+                    route["action"],
+                )
+                for route in json.load(f)
+            ]
+        return routes
+
+    def create_blueprint(self, app: Flask):
+        for route in self.routes():
+            dd = (
+                self.directory
+                / "controllers"
+                / f"{route.controller}_controller"
             )
-            clazz = getattr(mod, f"{controller.title()}Controller")
 
-            app.add_url_rule(
-                route["path"],
-                endpoint=f"{controller}.{route['action']}",
-                view_func=getattr(clazz(), route["action"]),
-                methods=[route["method"]],
+            obj = import_module(dd.as_posix().replace("/", "."))
+            controller = getattr(obj, f"{route.controller.title()}Controller")
+
+            blueprint = Blueprint(route.controller, route.controller)
+            blueprint.add_url_rule(
+                rule=route.path,
+                endpoint=route.action,
+                view_func=getattr(controller(), route.action),
+                methods=[route.method],
             )
+            app.register_blueprint(blueprint)
diff --git a/sample_app/app.py b/sample_app/app.py
new file mode 100644
index 0000000..d8963ec
--- /dev/null
+++ b/sample_app/app.py
@@ -0,0 +1,14 @@
+from flask import Flask
+from mvc_flask import FlaskMVC
+
+
+def create_app():
+    app = Flask(__name__)
+    return app
+
+
+if __name__ == "__main__":
+    app = create_app()
+    app.template_folder = "views"
+    FlaskMVC(app, ".")
+    app.run()
diff --git a/example/controllers/contact_controller.py b/sample_app/controllers/contact_controller.py
similarity index 100%
rename from example/controllers/contact_controller.py
rename to sample_app/controllers/contact_controller.py
diff --git a/sample_app/controllers/home_controller.py b/sample_app/controllers/home_controller.py
new file mode 100644
index 0000000..31c1a3f
--- /dev/null
+++ b/sample_app/controllers/home_controller.py
@@ -0,0 +1,6 @@
+from flask import render_template
+
+
+class HomeController:
+    def index(self):
+        return render_template("index.html")
diff --git a/example/routes.json b/sample_app/routes.json
similarity index 50%
rename from example/routes.json
rename to sample_app/routes.json
index 23e0718..ab30968 100644
--- a/example/routes.json
+++ b/sample_app/routes.json
@@ -5,18 +5,6 @@
     "controller": "home",
     "action": "index"
   },
-  {
-    "method": "get",
-    "path": "/new",
-    "controller": "home",
-    "action": "new"
-  },
-  {
-    "method": "post",
-    "path": "/create",
-    "controller": "home",
-    "action": "create"
-  },
   {
     "method": "GET",
     "path": "/contact",
diff --git a/sample_app/views/index.html b/sample_app/views/index.html
new file mode 100644
index 0000000..f76d8e9
--- /dev/null
+++ b/sample_app/views/index.html
@@ -0,0 +1,1 @@
+<h1>Hello, World!</h1>


Test patch:
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/fixtures.py b/tests/fixtures.py
index cca0e1c..9333d80 100644
--- a/tests/fixtures.py
+++ b/tests/fixtures.py
@@ -1,21 +1,20 @@
-from example.app import create_app
-from mvc_flask import FlaskMVC
+from sample_app.app import create_app
 from ward import fixture
+from mvc_flask import FlaskMVC
 
 
 @fixture
-def test_client():
+def test_app():
     app = create_app()
     app.testing = True
-    app_contenxt = app.test_request_context()
-    app_contenxt.push()
-
-    return app.test_client()
+    app.template_folder = "views"
+    FlaskMVC(app, directory="sample_app")
+    return app
 
 
 @fixture
-def test_app():
-    app = create_app()
-    app.testing = True
+def test_client(app=test_app):
+    app_contenxt = app.test_request_context()
+    app_contenxt.push()
 
-    return app
+    return app.test_client()
diff --git a/tests/test_routes.py b/tests/test_routes.py
index 7967097..4b5f3de 100644
--- a/tests/test_routes.py
+++ b/tests/test_routes.py
@@ -1,5 +1,5 @@
-from ward import test, expect, skip
-from fixtures import test_app
+from ward import test, expect
+from tests.fixtures import test_app, test_client
 
 
 @test("contains routes")
@@ -8,8 +8,6 @@ def _(app=test_app):
 
     expect.assert_in("/contact", routes, None)
     expect.assert_in("/", routes, None)
-    expect.assert_in("/new", routes, None)
-    expect.assert_in("/create", routes, None)
 
 
 @test("contains endpoints")
@@ -17,8 +15,6 @@ def _(app=test_app):
     endpoints = [route.endpoint for route in app.url_map.iter_rules()]
 
     expect.assert_in("home.index", endpoints, None)
-    expect.assert_in("home.new", endpoints, None)
-    expect.assert_in("home.create", endpoints, None)
     expect.assert_in("contact.index", endpoints, None)
 
 
@@ -30,5 +26,20 @@ def _(app=test_app):
         for route in routes.methods
     ]
 
-    expect.assert_equal(methods.count("GET"), 4, None)
-    expect.assert_equal(methods.count("POST"), 1, None)
+    expect.assert_equal(methods.count("GET"), 3, None)
+
+
+@test("must contains text")
+def _(client=test_client):
+    res = client.get("/")
+
+    expect.assert_equal(res.status_code, 200, None)
+    expect.assert_in("Hello, World!", res.get_data(as_text=True), None)
+
+
+@test("must contains text")
+def _(client=test_client):
+    res = client.get("/contact")
+
+    expect.assert_equal(res.status_code, 200, None)
+    expect.assert_in("Contact page", res.get_data(as_text=True), None)
diff --git a/tests/test_visitors_visit_routes.py b/tests/test_visitors_visit_routes.py
deleted file mode 100644
index f1dae3c..0000000
--- a/tests/test_visitors_visit_routes.py
+++ /dev/null
@@ -1,36 +0,0 @@
-from ward import test, expect
-from fixtures import test_client
-
-
-@test("must contains text")
-def _(client=test_client):
-    res = client.get("/")
-
-    expect.assert_equal(res.status_code, 200, None)
-    expect.assert_in("Hello, World!", res.get_data(as_text=True), None)
-
-
-@test("must contains text")
-def _(client=test_client):
-    res = client.get("/contact")
-
-    expect.assert_equal(res.status_code, 200, None)
-    expect.assert_in("Contact page", res.get_data(as_text=True), None)
-
-
-@test("must contains form")
-def _(client=test_client):
-    res = client.get("/new")
-
-    expect.assert_equal(res.status_code, 200, None)
-    expect.assert_in("form", res.get_data(as_text=True), None)
-    expect.assert_in("Send", res.get_data(as_text=True), None)
-
-
-@test("must redirect to index")
-def _(client=test_client):
-    res = client.post("/create", follow_redirects=True)
-
-    expect.assert_equal(res.status_code, 200, None)
-    expect.assert_equal(res.request.path, "/", None)
-    expect.assert_in("Hello, World!", res.get_data(as_text=True), None)