14
\$\begingroup\$

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

        def read_all():
            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
            new_files = read_all()
            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
                old_files = read_all()
            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"
\$\endgroup\$
4
  • 1
    \$\begingroup\$ 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 ;)) \$\endgroup\$ Mar 21, 2014 at 0:53
  • \$\begingroup\$ pyinotify relies on inotify, that does not work in this scenario, as I explained. \$\endgroup\$
    – Waiski
    Mar 21, 2014 at 5:38
  • \$\begingroup\$ 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. \$\endgroup\$
    – tbm
    Jan 27, 2018 at 6:04
  • 1
    \$\begingroup\$ 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/… \$\endgroup\$ Oct 14, 2020 at 17:12

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Browse other questions tagged or ask your own question.