# Django on virtual machine - watching changes in static files and autocompiling LESS without inotify

I'm setting up a Django development environment using Vagrant to run an Ubuntu virtual machine on VirtualBox. As this is a student project run mostly by very amateur coders and I want everyone to be able to contribute, I want the environment to be as convenient as possible.

What I'm trying to achieve:

• LESS-files would be compiled to CSS automatically whenever they are changed
• This automation would be run immediately on vagrant up, without any need to SSH the virtual machine.

I'm using django-pipeline (as it is supports Python3, unlike some other similar plugins) to manage my static files and compile LESS. Getting the files to compile automatically has proven a bit problematic though, since changing files in the shared folder from the host machine does not trigger inotify events on the VirtualBox guest machine. This rules out the most commonly used options for watching a folder for changes.

One option that I considered was using the Guard-gem with the -p (polling) option, but this would have required setting up Ruby, that I don't use elsewhere in the project, so I abandoned it as too complicated an option.

Inspired somewhat by Django's server's way of monitoring files in the absence of inotify, I decided to write my own script for watching for changes in a folder and then run that script on screen in Vagrant's provisioning script. I got it to work, but since I feel like I have no idea what I'm doing, I want to ask:

1. Does this solution make any sense at all or would there have been an insanely easy solution to the original problem that I just missed?
2. I'm a newbie to Python. Does my code comply to the best practices of using Django and Python?
3. Did I just crack an enigmatic riddle puzzling millions worldwide or should I just be embarrassed about ever spending time on such a petty problem?

## The code

myapp/management/commands/watchstatic.py:

from django.core.management.base import NoArgsCommand, CommandError
from django.core.management import call_command
from django.conf import settings
from pipeline.exceptions import CompilerError
import os
import sched
import time

class Command(NoArgsCommand):
help = 'Watches the static folder for changes, and runs the collectstatic-command if something is changed'
def handle_noargs(self, **options):

def get_time(filename):
stat = os.stat(filename)
mtime = stat.st_mtime
return mtime

def files_and_times(directory):
files = []
files_dict = {}
for (dirpath, dirname, filenames) in os.walk(directory):
for name in filenames:
files.append(dirpath + '/' + name)
for filename in files:
files_dict[filename] = get_time(filename)
return files_dict

all_files = {}
for key, sets in settings.WATCH_STATIC_FOLDERS.items():
folder_dict = files_and_times(sets['folder'])
all_files[key] = folder_dict
return all_files

def files_changed(sc, old_files={}):
# Read new files and their modification times
if new_files != old_files:
print('Files changed!')
try:
call_command('collectstatic', interactive=False)
# Delete the useless files created by django-pipeline
# See: https://github.com/cyberdelia/django-pipeline/issues/202
for key, sets in settings.WATCH_STATIC_FOLDERS.items():
for filename in sets['delete']:
filepath = sets['folder'] + '/' + filename
os.remove(filepath)
print('Deleted ' + filepath)
except CompilerError:
print('Failed to compile!')
# Just try again when the code is changed next time
pass
sc.enter(settings.WATCH_INTERVAL, 1, files_changed, (sc, old_files))

s = sched.scheduler(time.time, time.sleep)
# The files are always compiled on the first run
s.enter(settings.WATCH_INTERVAL, 1, files_changed, (s,))
s.run()


settings.py:

WATCH_INTERVAL = 5              # Seconds
WATCH_STATIC_FOLDERS = {
'myapp': {
'folder': 'myapp/static',
'delete': (
'bootstrap_less/bootstrap.css',
'style.css'
)
}
}


In Vagrant provisioning shell script:

su - vagrant -c "cd /vagrant && screen -S watcher -d -m python manage.py watchstatic"

• I'd recommend you look at using github.com/seb-m/pyinotify or similar for this; it's the "corrrect" way to do it (certainly far more correct than walking through every single file ;)) Mar 21 '14 at 0:53
• pyinotify relies on inotify, that does not work in this scenario, as I explained. Mar 21 '14 at 5:38
• I would avoid reinventing the wheel and use proven technology, in this case compass . Yes, it's ruby, but that's a one-line install so isn't too complex.
– tbm
Jan 27 '18 at 6:04
• Watchdog ( github.com/samuelcolvin/watchgod ) is Python changes watcher without utilizing inotify, I'd use that instead of reinventing a wheel and encountering various bugs during that. For example in here files_changed(sc, old_files={}) you have a dictionary as a default value which might cause issues, see docs.python-guide.org/writing/gotchas/… Oct 14 '20 at 17:12