Python, VirtualEnv, Flask Framework, Redis, Coffeescript, Sass. My Grunt workflow integration.

When developing the web app Memely.io, I chose a dev stack a bit outside of my norm as a bit of a R&D exercise. I wanted to explore some web frameworks for Python, and wanted to try out a framework that’s not as heavyweight as the dominant python framework juggernaut Django. A few searches later on StackOverflow, and I was lead towards Flask Framework. Within a quick peek of it’s documentation I felt right at home, as it seemed to be pretty similar to the PHP framework that I’m most familiar with - Slim Framework.

Within minutes I had a virtualenv environment set up and a ‘hello world’ page up and running thanks to Flask’s built in dev server, but there was one problem that was really making me miss a traditional LAMP stack website build. After a couple days of working on my project, I noticed this dev stack was slowing me down. I’ve got roughly half a dozen technologies powering this website, all of which require a terminal script to start their process, and get me on my way to working on my project. This was annoying, especially when I just want to do a quick 15 minute job on the website. It would take me a few minutes and sometimes some trips to a text file for the appropriate terminal commands, just to get a dev environment up and running before I could begin hacking.

Before you readers say it, I’ve tried it - Vagrant. Again, vagrant still takes time to boot, shut down, and still I’d have to build some solution to start up the python server with virtual env, and also start a redis server, which again is annoying and not really a win in my books.

Enter: Grunt

Having been getting pretty familar with the Javascript Task Runner, Grunt, I knew there must have been a solution - and indeed there was. Long story short, I’m able to active my python virtual environment, start the python server, start the redis server, start a coffeescript watch task, and also start a sass watch task, all with a simple ‘grunt start’ task I’ve defined. Long story long, here’s how it worked in the end:

I made use of the following NPM Tasks:

  • grunt-exec: for executing shell scripts
  • grunt-concurrent: for running multiple blocking scripts within 1 terminal window
  • grunt-contrib-watch: for watching changes of my coffeescript and sass files
  • grunt-newer: for only compiling coffeescript changed files - not the entire coffeescript directory

Here’s how that looks within my gruntfile:


//...
grunt.loadNpmTasks( "grunt-exec" );
grunt.loadNpmTasks( "grunt-concurrent" );
grunt.loadNpmTasks( "grunt-contrib-watch" );
grunt.loadNpmTasks( "grunt-newer" );
//...

I then defined 2 simple execution tasks to switch to the virtual environment and also start up a redis server. An important note, I tried doing the same with some other grunt execution tasks, such as grunt-shell and grunt-bgshell, both of which didn’t work properly, especially when it came to the virtualenv that’s required to make it all happen.


//...
exec: {

	startServer: {
		cmd: function() {
			return ". venv/bin/activate\npython app/App.py"
		}
	},

	redis: {
		cmd: function() {
			return "~/dev/redis/src/redis-server"
		}
	}

},
//...

Both of the scripts being executed above (exec:startServer and exec:redis) would normally require separate terminal windows, as they’re “blocking” scripts. In order to overcome this, we merge them together in 1 single terminal window using a concurrent task:


//...

concurrent: {
	devServer: {
		tasks: ["exec:startServer", "exec:redis", "watch"], //note, the watch task is defined elsewhere - see below for the finished file.
		options: {
			logConcurrentOutput: true
		}
	}
},

//...
 

Lastly, I defined my own grunt task so I can simply run ‘grunt start’ in order to get it all up and running.


//...
grunt.registerTask( "start", [ 
	"compass:dev",	//note, the compass and coffee tasks are defined elsewhere - see below for my finished file
	"coffee",
	"concurrent:devServer"
]);
//...

In the end, my finished grunt file looked like this, with the exclusion of my build process - which isn’t covered in this article:


"use strict";

module.exports = function( grunt ) {
	
	grunt.loadNpmTasks( "grunt-contrib-compass" );
	grunt.loadNpmTasks( "grunt-contrib-coffee" );
	grunt.loadNpmTasks( "grunt-contrib-watch" );
	grunt.loadNpmTasks( "grunt-concurrent" );
	grunt.loadNpmTasks( "grunt-newer" );
	grunt.loadNpmTasks( "grunt-exec" );

	grunt.initConfig( {

		exec: {

			startServer: {
				cmd: function() {
					return ". venv/bin/activate\npython app/App.py"
				}
			},

			redis: {
				cmd: function() {
					return "~/www/redis/src/redis-server"
				}
			}

		},

		concurrent: {
			devServer: {
				tasks: ["exec:startServer", "exec:redis", "watch"],
				options: {
					logConcurrentOutput: true
				}
			}
		},

		compass: {
			dev: {
				options: {
					basePath: "app",
					sassDir: "static/styles/sass",
					cssDir: "static/styles/css",
					environment: "development",
					imagesDir: "static/images",
					outputStyle: "expanded",
					noLineComments: true,
					relativeAssets: true,
					force: true
				}
			}
		},

		coffee: {		
			default: {
				options: {
					sourceMap: true
				},
				expand: true,
				cwd: "app/static/scripts/coffee/",
				src: "**/**.coffee",
				dest: "app/static/scripts/js",
				ext: ".js"
			}
		},
		watch: {
			compass: {
				files: [ "app/static/styles/sass/**/*.{scss,sass}" ],
				tasks: "compass:dev",
				options: {
					debounceDelay: 200
				}
			},
			coffee: {
				files: [ "app/static/scripts/coffee/**/*.coffee" ],
				tasks: "newer:coffee",
				options: {
					debounceDelay: 200
				}
			}
		}

	} );

	// Run compass, coffee, and start it all up
	grunt.registerTask( "start", [ 
		"compass:dev",
		"coffee",
		"concurrent:devServer"
	]);

	grunt.registerTask( "default", [ "start" ] );

};

Finally, when I’m done with working on the app for the day, a simple control+c is required to stop all the processes that had been executed with “grunt start”.

I Hope this helps someone as much as it’s helped me!

  1. mengxilu reblogged this from handlebarcreative
  2. handlebarcreative posted this