Splitting main.py module/file into several *.py files/modules
If you have followed this Flask series until this article, you will know that up until now our Flask article still lived inside a single file:
main.py. Is it a bad practice? Perhaps. But it really is fun. It is a proof that a web application can be contained within one single Python module, which is a realization of the initial April Fools Joke that drive the creation of our beloved Flask web framework: it's possible to create such, but certainly not advisable.
Having said that, Flask still don't impose any strict directory organization or modules separation. Compare this with Django, where it come up with a monolithinc ready to use project directory that you must adhere, in Flask world, you can freely to choose to follow a simple organization of your Flask application into package as this official guide shows you, or turning your Flask application into a highly modular application using Blueprints. In this article we are going to implement a much more simplified version of Flask project directory organization : splitting
main.py module into several
*.py files/modules that wrap logically related class/code into its own module. The idea is simple: e.g., if you want to add a new form, you don't have to scroll through the code that talks about models. In the next subsequent article, we will then modularize our application using Blueprints.
Let's get started!
Although this Flask Tutorial series talk specifically of using Openshift as the deployment platform, but as I have intention to discuss another PaaS solution (such as Heroku, Google Cloud, etc), I would like to give a wider view of the process of restructuring our Flask application.
In a framework that implement the standard WSGI specification, your application will going to have a single Python file that act as your application startup file (a good practical hands-on with WSGI can be found in Flask's author page here). If you are using another PaaS solution beside Openshift, try to find this file. Whereas in Openshift's Python application, this startup file is reside in
wsgi/application. From the standard
wsgi/application provided by Openshift, I have modified this file into this:
1 2 3 4 5 6 7 8 9 10 11 12 13
#!/usr/bin/python import os virtenv = os.environ['OPENSHIFT_PYTHON_DIR'] + '/virtenv/' os.environ['PYTHON_EGG_CACHE'] = os.path.join(virtenv, 'lib/python2.7/site-packages') virtualenv = os.path.join(virtenv, 'bin/activate_this.py') try: execfile(virtualenv, dict(__file__=virtualenv)) except IOError: pass from main import * dbinit()
From the PEP 0333 that define what WSGI is, we have this definition of what constitute a WSGI application : "The application object is simply a callable object that accepts two arguments. The term "object" should not be misconstrued as requiring an actual object instance: a function, method, class, or instance with a __call__ method are all acceptable for use as an application object". Therefore, in our current single file
main.py, we instantiate our
Flask object with the name
application, making our application as a fully qualified WSGI application: runnable by uWSGI server being used by Openshift to serve our Python application.
Have a look at the above
wsgi/application. After activating Virtual Environment used by our application, we then import
main.py file, making this
application available with the following line in
application = Flask(__name__)
Therefore, the core work of re-organization of our Flask Biography application should be done in this
Have a look at our current
main.py file. At the current state, we throw all kinds of classes, objects and functions there. But however it intertwined, we still able to recognize these several kind of objects:
- Configuration: Line 18 to Line 26
- Models: Line 46 to Line 93
- Forms: Line 96 to Line 131
- Routes/Views: Login manager code from Line 30 to Line 43 and route/views code from Line 133 to Line 335.
Those are perfect example of objects that need to be separated into its own
*.py file and later must be imported into a new
main.py to compose a valid Flask
object as in the previous
main.py file. Using the above objects separation's list complete with its line of code part, we will be able to easily refactor it into a new
*.py file/module. Below are the result of the above objects separation into several Python modules:
main.py: this module will now have a simple role of:
Remember, that the code after
Instantiating our Flask
Loading our Flask views/routes.
if __name__ == '__main__':that will run the builtin WSGI server, will only get executed if this module was called from command line using the command:
python main.py. As our Flask application already being handled by Openshift uWSGI server, this
main.pymodule will only get imported by
config.py: as you can see from the
Flaskclass is having a
configobject that can load its properties from a class with
from_object()method. This will make us easier in storing configurations into separate python modules. If you want to store sensitive information there, you can later put this
.gitignorefile making this code will never get published into central git repository. Or, in my current approach, I utilize system environment variable to store sensitive information (e.g. connection string to database, complete with its username and password).
models.py: this is where all of our Flask models were stored. Please inspect that this module is not imported from within
main.py, instead it is imported from within
views.py, that is because we use our application models intensively when we respond to user requests.
forms.py: contains all of our WTForms classes, thas was also imported from within
views.py: the core part of this application, which consist of all route/views method that will respond to users request.
As long as you know how to import Python modules, the above task is rather trivial.
But our job is not done yet. Have a look at our
views.py. At the current state, that file is still a mess. It still is a storage of all routes/views method without any organization or modularization whatsoever. The following section will talk about how to modularize this
views module using Flask-Classy. Instead of using regular functions as your routes/views method, finally you can use an OOP approach to manage your routes/views method. This new level of modularization is the important thing from Flask-Classy that gives Flask web development a better way of handling application complexity.
Let see how we restructure our
views module using Flask-Classy.
Restructuring Views Modules Using Flask-Classy
A quote from Flask-Classy homepage: "I ❤ Flask. Like a lot. But sometimes projects get a little big and I need some way of managing and organizing all the different pieces. I know what you’re saying: “But what about Blueprints?” You’re right. Blueprints are pretty awesome. But I found that they aren’t always enough to encapsulate a specific context the way I need. What I wanted, no what I needed was to be able to group my views into relevant classes each with their own context and behavior. It’s also made testing really nifty too".
The underlined part of the quote is a neat definition of what Flask Classy is.
First of all, lets inspect what our
views module looks like. As I am using PyCharm, I can easily inspect the current structure of
views module by pressing CTRL+F12 to popup the file structure window, as seen below:
A flat structure of views module
The structure given by the picture above is a flat structure: all items were on the same level. No grouping or hierarchical structure imposed. Although all the function were using pretty names that make us realize its logical grouping, there is no actual grouping there. We just got lucky that Jetbrans PyCharm display our method in alphabetical order.
For the sake of our example, lets take all the function that serve as routes/views method for editing users biography, and restructure them using Flask-Classy. I am sure you have already know it, but for clarification, I will just pasted all of those functions --in stripped form to conserve space-- below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
@application.route('/user_edit_biography', methods=['POST']) def user_edit_biography(): pass @application.route('/user_edit_fullname', methods=['POST']) def user_edit_fullname(): pass @application.route('/user_edit_tagline', methods=[ 'POST']) def user_edit_tagline(): pass @application.route('/user_upload_avatar', methods=['POST']) def user_upload_avatar(): pass
Observe that for each method we specifically set its route name, e.g we set route for URL
/user_edit_fullname to be handled by function
user_edit_fullname(). Although it's intentionally for the route name and the function name to use the same name, you are free to use different name though.
In order to introduce a new class using Flask-Classy for the above methods, you will need to first install Flask-Classy package into your Python environment using the command
pip install Flask-Classy==0.6.3. And if you do use Openshift, don't forget to add this requirement into
setup.py. Having all prepared, lets start creating this class below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
class BiographyView(FlaskView): @route('edit_biography', methods=['POST']) def edit_biography(self): pass @route('edit_fullname', methods=['POST']) def edit_fullname(self): pass @route('edit_tagline', methods=['POST']) def edit_tagline(self): pass @route('upload_avatar', methods=['POST']) def upload_avatar(self): pass BiographyView.register(application)
BiographyView class which is a new class derived from Flask-Classy
FlaskView as our class parent, that will serve as class for all methods that dealt with editing users Biography. For each member method, we decorate it with Flask-Classy
route decorator to set its route name and method type. Actually, if your method type is GET, you can omitted the
route decorator. But, as we do need to set our method to use POST method type, we have to specifically use
route decorator, that needs the first argument to be the route path of this method.
Finally, we register this new view class into our one and only Flask application instance. This will make request having our defined route will be handled by our designated class member method. The default behaviour of Flask-Classy, is to create the URL with the format
/<first_phrase_of_class_name_in_lower_case>/<full_method_name_in_lower_case>. As I am okay with that behaviour, I don't need to specifically set route prefix or route_base to a custom value. Hence, we will have a new route named :
The last modification that we need is to change all
url_for() or manual hand coded URL in the existing template from the previous form into this new form. For example, if in the previous
base_layout.html we use
url_for('signin'), now after we have create an
AuthView class to group all authentication related request, we will have to change it into
url_for('AuthView:signin'). As for manual hand coded URL changes, you can see the change from this into this.
If you inspect the structure of the
views modules using PyCharm, you will have image as depicted below:
Structure of views module after using Flask-Classy to manage its views complexity
Although there are still dangling functions there, where some are needed by Flask-Login to remain that way, now we already have a nice structure of
views module: you can clearly see that our modules already handle Authentication, Biography, Portfolio and Settings. The future task of adding new routes/views will become easier and more maintainable after we introduce Flask Classy to our project.
The intention of this part of the series is to prepare our project for a bigger application scale. The previous single module/file approach is not suited for a large scale application. After splitting the
main module, we then restructure
views module even more by organizing all of routes/views method into classes using Flask-Classy. This makes the code more maintainable.
There is another approach of making our Flask project more modular: using Blueprints. I will revisit this application by implementing Blueprints to make a modular application development approach to our existing Flask application. Among other things, using Blueprints will make our application easier to be enhance as a web API.
As always you can download the current state of the application here : bio-part-13.zip.
Or follow its public Github repository here : pythonthusiast/bio.
Stay tune for my next article!